core: Zabbix integration unable to login.

The problem

I ran the zabbix integration for over a year now, and since a few months I get the error “Unable to login to the zabbix api”. I ran Zabbix 6.4 beta 3 with succes, but since beta 4 the message appears so I guess they made changes to the api.

What version of Home Assistant Core has the issue?

2023.1.7

What was the last working version of Home Assistant Core?

No response

What type of installation are you running?

Home Assistant OS

Integration causing the issue

Zabbix

Link to integration documentation on our website

https://www.home-assistant.io/integrations/zabbix/

Diagnostics information

No response

Example YAML snippet

zabbix:
  host: xxx.xxx.xxx.xxx
  path: /
  ssl: false
  username: Admin
  password: ***
  publish_states_host: hostname-of-has
  exclude:
    domains:
      - device_tracker
    entities:
      - sun.sun
      - sensor.time

Anything in the logs that might be useful for us?

2023-01-29 07:22:17.741 ERROR (SyncWorker_5) [homeassistant.components.zabbix] Unable to login to the Zabbix API: {'code': -32602, 'message': 'Invalid params.', 'data': 'Invalid parameter "/": unexpected parameter "user".', 'json': "{'jsonrpc': '2.0', 'method': 'user.login', 'params': {'user': 'Admin', 'password': '********'}, 'id': '1'}"}
2023-01-29 07:22:17.830 ERROR (MainThread) [homeassistant.setup] Setup failed for zabbix: Integration failed to initialize.

Additional information

No response

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 6
  • Comments: 100 (19 by maintainers)

Most upvoted comments

@kornkuu

I got this working temporarily while I work to rewrite the code to use a maintained module.

Are you actually working on a “good” fix? Using pyzabbix?

Yes, I am working to rewrite the integration to work with that module.

I get it working against Zabbix 6.4 by patching the file pyzabbix/api.py

However the simplest fix would be to change underlaying library from the current (unmaintained) https://github.com/adubkov/py-zabbix

to newer (maintained) https://github.com/lukecyca/pyzabbix/

This replacement will fix the current issue and in addition will also allow to use the Apikey authentication instead.

Yes, you’re right, this seems to be the more strightforward and stable way (from my personal today’s view at least…)

What I have in mind (additionally) is some discovery that

  1. automatically creates the items in Zabbix (based on the json containing every entity)
  2. with the same names as now (using the zabbix ha integration).

So I can neatlessly switch…

I have HAOS on Yellow and no pyzabbix file exist, so I can’t edit the params to use the Zabbix components as mentioned in this video https://www.youtube.com/watch?v=6G_wec2pLw4. Any ideas?

I had the same problem, and in the end, I went the API route. And ultimately, it’s a better solution for me. I’ve created a brief tutorial:

Monitoring HA in Zabbix

I have created https://github.com/home-assistant/core/pull/110132 if somebody wants to review it 😃

Just after after I was done implementing the zabbix api and the user login I realized that this is completely unnecessary if you only want to send metrics to zabbix … 🤦

Me, I am NOT using the number of active triggers. I am only using the sensors being mirrored from ha to zabbix.

Yes, I started with this one, with the devcontainers via WSL. This is how I was testing the PRs. But there are a lot of other to read. And this one from Config Flow - “Integrations with a config flow require full test coverage of all code in config_flow.py to be accepted into core.” raises the bar even higher for the begginers.

Anyway, I’ll try, but no promises about the timeline.

it should be “… from the unmaintained py-zabbix library to the maintained zabbix library …”

Thanks for your tests! Yep. Will update. I will need to make a seperate PR to update the documentation for homeassistant anyways.

P.S. Do you want me to copy the same to the PR comments / reviews?

No, this should be OK. Reviews are for the maintainers of homeassistant. They will probably have something to remark as well 😃

Hi @WebSpider I’ve tested the PR and it is working fine. I am able to receive the information to the zabbix server from the HA running with this PR. However I have comment on the PR description - “This moves the support for zabbix from the unmaintained py-zabbix library to the maintained pyzabbix library…” is not correct, it should be “… from the unmaintained py-zabbix library to the maintained zabbix library …” as in manifest.json it is now "requirements": ["zabbix==2.0.2"].

P.S. Do you want me to copy the same to the PR comments / reviews?

I’m working on a PR today, apologies for the delay 😃

Why don’t we use a parameter in config file? Like this: zbx_version: x.xx

The version of Zabbix server is supported and available prior to any authentication calls: https://www.zabbix.com/documentation/current/en/manual/api/reference/apiinfo/version

and is also properly handled in the newer pyzabix library implementation: https://github.com/lukecyca/pyzabbix

Douing a deeper dive into this

https://github.com/adubkov/py-zabbix/pull/155

In all fairnerss would be a fix, The maintainer seems to be gone offline on GH tho.

the way you access and patch this file doesn’t really matter in our current case

Why? This works for me! I have restarted home assistant many times. The fix works for me

…I assume you refer to your post above

For now, I use the HAS Api with the Zabbix http agent which works really well. Make sure to generate a lifetime bearer apikey.

and you are using the Zabbix agent to access to Homeassistant API. Correct? Do you have some helpful hints or links to start with? That would be great.

Yes, you can use the http agent funtion within Zabbix to connect to the home assistant api. I think it is really a preferred way to get your data as home assistant otherwise spits a lot of useless data into your zabbix instance. I have started the creation of a template set you can use within Zabbix.

Blog post. Git Link.

This way you are certain to get just the data that you want. Secondly, you don’t depend on third party python modules anymore.

Do I am not a python dev, this is the first time I’ve written more than 1 line of Python. I found that py-zabbix 1.1.7 doesn’t have support for the api_token. However another conflicting library pyzabbix 1.2.1 does, however it doesn’t seem that the classes continue to work when mashed together.

I created a custom zabbix component with the below settings, and I had to manually include the logger and sender from py-zabbix

This loads without an error as modified, I don’t yet know if it’s sending states to zabbix, but it does create the sensors in Homeassistant if specified in sensors config.

It would be nice if this created a sensor for each trigger, rather than for all triggers on each host. Also, it doesn’t support ipv6 due to the ZabbixMetric class being from py-zabbix 1.1.7 which is years old without update

zabbix.yaml

host: x.x.x.x
path: "/"
ssl: false
api_version: "6.4"
api_token: !secret zabbix
publish_states_host: homeassistant
include:
  entity_globs:
    - sensor.*filter_life*

manifest.json

{
  "domain": "zabbix",
  "name": "Zabbix",
  "codeowners": [],
  "documentation": "https://www.home-assistant.io/integrations/zabbix",
  "iot_class": "local_polling",
  "loggers": ["pyzabbix"],
  "requirements": ["py-zabbix==1.1.7","pyzabbix==1.2.1"],
  "version": "0.0.0"
}

__init__.py

"""Support for Zabbix."""
from contextlib import suppress
import json
import logging
import math
import queue
import threading
import time
from urllib.error import HTTPError
from urllib.parse import urljoin

from pyzabbix import ZabbixAPI, ZabbixAPIException
from .sender import ZabbixMetric, ZabbixSender
import voluptuous as vol

from homeassistant.const import (
    CONF_HOST,
    CONF_PASSWORD,
    CONF_PATH,
    CONF_API_TOKEN,
    CONF_API_VERSION,
    CONF_SSL,
    CONF_USERNAME,
    EVENT_HOMEASSISTANT_STOP,
    EVENT_STATE_CHANGED,
    STATE_UNAVAILABLE,
    STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import event as event_helper, state as state_helper
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import (
    INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA,
    convert_include_exclude_filter,
)
from homeassistant.helpers.typing import ConfigType

_LOGGER = logging.getLogger(__name__)

CONF_PUBLISH_STATES_HOST = "publish_states_host"

DEFAULT_SSL = False
DEFAULT_PATH = "zabbix"
DEFAULT_API = "0.0"
DOMAIN = "zabbix"

TIMEOUT = 5
RETRY_DELAY = 20
QUEUE_BACKLOG_SECONDS = 30
RETRY_INTERVAL = 60  # seconds
RETRY_MESSAGE = f"%s Retrying in {RETRY_INTERVAL} seconds."

BATCH_TIMEOUT = 1
BATCH_BUFFER_SIZE = 100

CONFIG_SCHEMA = vol.Schema(
    {
        DOMAIN: INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA.extend(
            {
                vol.Required(CONF_HOST): cv.string,
                vol.Required(CONF_API_VERSION, default=DEFAULT_API): cv.string,
                vol.Optional(CONF_PASSWORD): cv.string,
                vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
                vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
                vol.Optional(CONF_USERNAME): cv.string,
                vol.Optional(CONF_API_TOKEN): cv.string,
                vol.Optional(CONF_PUBLISH_STATES_HOST): cv.string,
            }
        )
    },
    extra=vol.ALLOW_EXTRA,
)


def setup(hass: HomeAssistant, config: ConfigType) -> bool:
    """Set up the Zabbix component."""

    conf = config[DOMAIN]
    protocol = "https" if conf[CONF_SSL] else "http"

    url = urljoin(f"{protocol}://{conf[CONF_HOST]}", conf[CONF_PATH])
    apiversion = conf.get(CONF_API_VERSION)
    username = conf.get(CONF_USERNAME)
    password = conf.get(CONF_PASSWORD)
    apitoken = conf.get(CONF_API_TOKEN)

    publish_states_host = conf.get(CONF_PUBLISH_STATES_HOST)

    entities_filter = convert_include_exclude_filter(conf)

    try:
        if apiversion == "6.4":
            zapi = ZabbixAPI(url)
            zapi.login(api_token=apitoken)
        else: 
            zapi = ZabbixAPI(url)
            zapi.login(username, password)
        _LOGGER.info("Connected to Zabbix API Version %s", zapi.api_version())
    except ZabbixAPIException as login_exception:
        _LOGGER.error("Unable to login to the Zabbix API: %s", login_exception)
        return False
    except HTTPError as http_error:
        _LOGGER.error("HTTPError when connecting to Zabbix API: %s", http_error)
        zapi = None
        _LOGGER.error(RETRY_MESSAGE, http_error)
        event_helper.call_later(
            hass,
            RETRY_INTERVAL,
            lambda _: setup(hass, config),  # type: ignore[arg-type,return-value]
        )
        return True

    hass.data[DOMAIN] = zapi

    def event_to_metrics(event, float_keys, string_keys):
        """Add an event to the outgoing Zabbix list."""
        state = event.data.get("new_state")
        if state is None or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE):
            return

        entity_id = state.entity_id
        if not entities_filter(entity_id):
            return

        floats = {}
        strings = {}
        try:
            _state_as_value = float(state.state)
            floats[entity_id] = _state_as_value
        except ValueError:
            try:
                _state_as_value = float(state_helper.state_as_number(state))
                floats[entity_id] = _state_as_value
            except ValueError:
                strings[entity_id] = state.state

        for key, value in state.attributes.items():
            # For each value we try to cast it as float
            # But if we cannot do it we store the value
            # as string
            attribute_id = f"{entity_id}/{key}"
            try:
                float_value = float(value)
            except (ValueError, TypeError):
                float_value = None
            if float_value is None or not math.isfinite(float_value):
                strings[attribute_id] = str(value)
            else:
                floats[attribute_id] = float_value

        metrics = []
        float_keys_count = len(float_keys)
        float_keys.update(floats)
        if len(float_keys) != float_keys_count:
            floats_discovery = []
            for float_key in float_keys:
                floats_discovery.append({"{#KEY}": float_key})
            metric = ZabbixMetric(
                publish_states_host,
                "homeassistant.floats_discovery",
                json.dumps(floats_discovery),
            )
            metrics.append(metric)
        for key, value in floats.items():
            metric = ZabbixMetric(
                publish_states_host, f"homeassistant.float[{key}]", value
            )
            metrics.append(metric)

        string_keys.update(strings)
        return metrics

    if publish_states_host:
        zabbix_sender = ZabbixSender(zabbix_server=conf[CONF_HOST])
        instance = ZabbixThread(hass, zabbix_sender, event_to_metrics)
        instance.setup(hass)

    return True


class ZabbixThread(threading.Thread):
    """A threaded event handler class."""

    MAX_TRIES = 3

    def __init__(self, hass, zabbix_sender, event_to_metrics):
        """Initialize the listener."""
        threading.Thread.__init__(self, name="Zabbix")
        self.queue = queue.Queue()
        self.zabbix_sender = zabbix_sender
        self.event_to_metrics = event_to_metrics
        self.write_errors = 0
        self.shutdown = False
        self.float_keys = set()
        self.string_keys = set()

    def setup(self, hass):
        """Set up the thread and start it."""
        hass.bus.listen(EVENT_STATE_CHANGED, self._event_listener)
        hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self._shutdown)
        self.start()
        _LOGGER.debug("Started publishing state changes to Zabbix")

    def _shutdown(self, event):
        """Shut down the thread."""
        self.queue.put(None)
        self.join()

    @callback
    def _event_listener(self, event):
        """Listen for new messages on the bus and queue them for Zabbix."""
        item = (time.monotonic(), event)
        self.queue.put(item)

    def get_metrics(self):
        """Return a batch of events formatted for writing."""
        queue_seconds = QUEUE_BACKLOG_SECONDS + self.MAX_TRIES * RETRY_DELAY

        count = 0
        metrics = []

        dropped = 0

        with suppress(queue.Empty):
            while len(metrics) < BATCH_BUFFER_SIZE and not self.shutdown:
                timeout = None if count == 0 else BATCH_TIMEOUT
                item = self.queue.get(timeout=timeout)
                count += 1

                if item is None:
                    self.shutdown = True
                else:
                    timestamp, event = item
                    age = time.monotonic() - timestamp

                    if age < queue_seconds:
                        event_metrics = self.event_to_metrics(
                            event, self.float_keys, self.string_keys
                        )
                        if event_metrics:
                            metrics += event_metrics
                    else:
                        dropped += 1

        if dropped:
            _LOGGER.warning("Catching up, dropped %d old events", dropped)

        return count, metrics

    def write_to_zabbix(self, metrics):
        """Write preprocessed events to zabbix, with retry."""

        for retry in range(self.MAX_TRIES + 1):
            try:
                self.zabbix_sender.send(metrics)

                if self.write_errors:
                    _LOGGER.error("Resumed, lost %d events", self.write_errors)
                    self.write_errors = 0

                _LOGGER.debug("Wrote %d metrics", len(metrics))
                break
            except OSError as err:
                if retry < self.MAX_TRIES:
                    time.sleep(RETRY_DELAY)
                else:
                    if not self.write_errors:
                        _LOGGER.error("Write error: %s", err)
                    self.write_errors += len(metrics)

    def run(self):
        """Process incoming events."""
        while not self.shutdown:
            count, metrics = self.get_metrics()
            if metrics:
                self.write_to_zabbix(metrics)
            for _ in range(count):
                self.queue.task_done()

sender.py

# -*- encoding: utf-8 -*-
#
# Copyright © 2014 Alexey Dubkov
#
# This file is part of py-zabbix.
#
# Py-zabbix is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Py-zabbix is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with py-zabbix. If not, see <http://www.gnu.org/licenses/>.

from decimal import Decimal
import inspect
import json
import logging
import socket
import struct
import re

# For python 2 and 3 compatibility
try:
    from StringIO import StringIO
    import ConfigParser as configparser
except ImportError:
    from io import StringIO
    import configparser

from .logger import NullHandler

null_handler = NullHandler()
logger = logging.getLogger(__name__)
logger.addHandler(null_handler)


class ZabbixResponse(object):
    """The :class:`ZabbixResponse` contains the parsed response from Zabbix.
    """
    def __init__(self):
        self._processed = 0
        self._failed = 0
        self._total = 0
        self._time = 0
        self._chunk = 0
        pattern = (r'[Pp]rocessed:? (\d*);? [Ff]ailed:? (\d*);? '
                   r'[Tt]otal:? (\d*);? [Ss]econds spent:? (\d*\.\d*)')
        self._regex = re.compile(pattern)

    def __repr__(self):
        """Represent detailed ZabbixResponse view."""
        result = json.dumps({'processed': self._processed,
                             'failed': self._failed,
                             'total': self._total,
                             'time': str(self._time),
                             'chunk': self._chunk})
        return result

    def parse(self, response):
        """Parse zabbix response."""
        info = response.get('info')
        res = self._regex.search(info)

        self._processed += int(res.group(1))
        self._failed += int(res.group(2))
        self._total += int(res.group(3))
        self._time += Decimal(res.group(4))
        self._chunk += 1

    @property
    def processed(self):
        return self._processed

    @property
    def failed(self):
        return self._failed

    @property
    def total(self):
        return self._total

    @property
    def time(self):
        return self._time

    @property
    def chunk(self):
        return self._chunk


class ZabbixMetric(object):
    """The :class:`ZabbixMetric` contain one metric for zabbix server.

    :type host: str
    :param host: Hostname as it displayed in Zabbix.

    :type key: str
    :param key: Key by which you will identify this metric.

    :type value: str
    :param value: Metric value.

    :type clock: int
    :param clock: Unix timestamp. Current time will used if not specified.

    >>> from pyzabbix import ZabbixMetric
    >>> ZabbixMetric('localhost', 'cpu[usage]', 20)
    """

    def __init__(self, host, key, value, clock=None):
        self.host = str(host)
        self.key = str(key)
        self.value = str(value)
        if clock:
            if isinstance(clock, (float, int)):
                self.clock = int(clock)
            else:
                raise ValueError('Clock must be time in unixtime format')

    def __repr__(self):
        """Represent detailed ZabbixMetric view."""

        result = json.dumps(self.__dict__, ensure_ascii=False)
        logger.debug('%s: %s', self.__class__.__name__, result)

        return result


class ZabbixSender(object):
    """The :class:`ZabbixSender` send metrics to Zabbix server.

    Implementation of
    `zabbix protocol <https://www.zabbix.com/documentation/1.8/protocols>`_.

    :type zabbix_server: str
    :param zabbix_server: Zabbix server ip address. Default: `127.0.0.1`

    :type zabbix_port: int
    :param zabbix_port: Zabbix server port. Default: `10051`

    :type use_config: str
    :param use_config: Path to zabbix_agentd.conf file to load settings from.
         If value is `True` then default config path will used:
         /etc/zabbix/zabbix_agentd.conf

    :type chunk_size: int
    :param chunk_size: Number of metrics send to the server at one time

    :type socket_wrapper: function
    :param socket_wrapper: to provide a socket wrapper function to be used to
         wrap the socket connection to zabbix.
         Example:
            from pyzabbix import ZabbixSender
            import ssl
            secure_connection_option = dict(..)
            zs = ZabbixSender(
                zabbix_server=zabbix_server,
                zabbix_port=zabbix_port,
                socket_wrapper=lambda sock:ssl.wrap_socket(sock,**secure_connection_option)
            )

    :type timeout: int
    :param timeout: Number of seconds before call to Zabbix server times out
         Default: 10
    >>> from pyzabbix import ZabbixMetric, ZabbixSender
    >>> metrics = []
    >>> m = ZabbixMetric('localhost', 'cpu[usage]', 20)
    >>> metrics.append(m)
    >>> zbx = ZabbixSender('127.0.0.1')
    >>> zbx.send(metrics)
    """

    def __init__(self,
                 zabbix_server='127.0.0.1',
                 zabbix_port=10051,
                 use_config=None,
                 chunk_size=250,
                 socket_wrapper=None,
                 timeout=10):

        self.chunk_size = chunk_size
        self.timeout = timeout

        self.socket_wrapper = socket_wrapper
        if use_config:
            self.zabbix_uri = self._load_from_config(use_config)
        else:
            self.zabbix_uri = [(zabbix_server, zabbix_port)]

    def __repr__(self):
        """Represent detailed ZabbixSender view."""

        result = json.dumps(self.__dict__, ensure_ascii=False)
        logger.debug('%s: %s', self.__class__.__name__, result)

        return result

    def _load_from_config(self, config_file):
        """Load zabbix server IP address and port from zabbix agent config
        file.

        If ServerActive variable is not found in the file, it will
        use the default: 127.0.0.1:10051

        :type config_file: str
        :param use_config: Path to zabbix_agentd.conf file to load settings
            from. If value is `True` then default config path will used:
            /etc/zabbix/zabbix_agentd.conf
        """

        if config_file and isinstance(config_file, bool):
            config_file = '/etc/zabbix/zabbix_agentd.conf'

        logger.debug("Used config: %s", config_file)

        #  This is workaround for config wile without sections
        with open(config_file, 'r') as f:
            config_file_data = "[root]\n" + f.read()

        params = {}

        try:
            # python2
            args = inspect.getargspec(
                configparser.RawConfigParser.__init__).args
        except ValueError:
            # python3
            args = inspect.getfullargspec(
                configparser.RawConfigParser.__init__).kwonlyargs

        if 'strict' in args:
            params['strict'] = False

        config_file_fp = StringIO(config_file_data)
        config = configparser.RawConfigParser(**params)
        config.readfp(config_file_fp)
        # Prefer ServerActive, then try Server and fallback to defaults
        if config.has_option('root', 'ServerActive'):
            zabbix_serveractives = config.get('root', 'ServerActive')
        elif config.has_option('root', 'Server'):
            zabbix_serveractives = config.get('root', 'Server')
        else:
            zabbix_serveractives = '127.0.0.1:10051'

        result = []
        for serverport in zabbix_serveractives.split(','):
            if ':' not in serverport:
                serverport = "%s:%s" % (serverport.strip(), 10051)
            server, port = serverport.split(':')
            serverport = (server, int(port))
            result.append(serverport)
        logger.debug("Loaded params: %s", result)

        return result

    def _receive(self, sock, count):
        """Reads socket to receive data from zabbix server.

        :type socket: :class:`socket._socketobject`
        :param socket: Socket to read.

        :type count: int
        :param count: Number of bytes to read from socket.
        """

        buf = b''

        while len(buf) < count:
            chunk = sock.recv(count - len(buf))
            if not chunk:
                break
            buf += chunk

        return buf

    def _create_messages(self, metrics):
        """Create a list of zabbix messages from a list of ZabbixMetrics.

        :type metrics_array: list
        :param metrics_array: List of :class:`zabbix.sender.ZabbixMetric`.

        :rtype: list
        :return: List of zabbix messages.
        """

        messages = []

        # Fill the list of messages
        for m in metrics:
            messages.append(str(m))

        logger.debug('Messages: %s', messages)

        return messages

    def _create_request(self, messages):
        """Create a formatted request to zabbix from a list of messages.

        :type messages: list
        :param messages: List of zabbix messages

        :rtype: list
        :return: Formatted zabbix request
        """

        msg = ','.join(messages)
        request = '{{"request":"sender data","data":[{msg}]}}'.format(msg=msg)
        request = request.encode("utf-8")
        logger.debug('Request: %s', request)

        return request

    def _create_packet(self, request):
        """Create a formatted packet from a request.

        :type request: str
        :param request: Formatted zabbix request

        :rtype: str
        :return: Data packet for zabbix
        """

        data_len = struct.pack('<Q', len(request))
        packet = b'ZBXD\x01' + data_len + request

        def ord23(x):
            if not isinstance(x, int):
                return ord(x)
            else:
                return x

        logger.debug('Packet [str]: %s', packet)
        logger.debug('Packet [hex]: %s',
                     ':'.join(hex(ord23(x))[2:] for x in packet))
        return packet

    def _get_response(self, connection):
        """Get response from zabbix server, reads from self.socket.

        :type connection: :class:`socket._socketobject`
        :param connection: Socket to read.

        :rtype: dict
        :return: Response from zabbix server or False in case of error.
        """

        response_header = self._receive(connection, 13)
        logger.debug('Response header: %s', response_header)

        if (not response_header.startswith(b'ZBXD\x01') or
                len(response_header) != 13):
            logger.debug('Zabbix return not valid response.')
            result = False
        else:
            response_len = struct.unpack('<Q', response_header[5:])[0]
            response_body = connection.recv(response_len)
            result = json.loads(response_body.decode("utf-8"))
            logger.debug('Data received: %s', result)

        try:
            connection.close()
        except socket.error:
            pass

        return result

    def _chunk_send(self, metrics):
        """Send the one chunk metrics to zabbix server.

        :type metrics: list
        :param metrics: List of :class:`zabbix.sender.ZabbixMetric` to send
            to Zabbix

        :rtype: str
        :return: Response from Zabbix Server
        """
        messages = self._create_messages(metrics)
        request = self._create_request(messages)
        packet = self._create_packet(request)

        for host_addr in self.zabbix_uri:
            logger.debug('Sending data to %s', host_addr)

            try:
                # IPv4
                connection_ = socket.socket(socket.AF_INET)
            except socket.error:
                # IPv6
                try:
                    connection_ = socket.socket(socket.AF_INET6)
                except socket.error:
                    raise Exception("Error creating socket for {host_addr}".format(host_addr=host_addr))
            if self.socket_wrapper:
                connection = self.socket_wrapper(connection_)
            else:
                connection = connection_

            connection.settimeout(self.timeout)

            try:
                # server and port must be tuple
                connection.connect(host_addr)
                connection.sendall(packet)
            except socket.timeout:
                logger.error('Sending failed: Connection to %s timed out after'
                             '%d seconds', host_addr, self.timeout)
                connection.close()
                raise socket.timeout
            except socket.error as err:
                # In case of error we should close connection, otherwise
                # we will close it after data will be received.
                logger.warning('Sending failed: %s', getattr(err, 'msg', str(err)))
                connection.close()
                raise err

            response = self._get_response(connection)
            logger.debug('%s response: %s', host_addr, response)

            if response and response.get('response') != 'success':
                logger.debug('Response error: %s}', response)
                raise socket.error(response)

        return response

    def send(self, metrics):
        """Send the metrics to zabbix server.

        :type metrics: list
        :param metrics: List of :class:`zabbix.sender.ZabbixMetric` to send
            to Zabbix

        :rtype: :class:`pyzabbix.sender.ZabbixResponse`
        :return: Parsed response from Zabbix Server
        """
        result = ZabbixResponse()
        for m in range(0, len(metrics), self.chunk_size):
            result.parse(self._chunk_send(metrics[m:m + self.chunk_size]))
        return result

logger.py

# -*- encoding: utf-8 -*-
#
# Copyright © 2014 Alexey Dubkov
#
# This file is part of py-zabbix.
#
# Py-zabbix is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Py-zabbix is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with py-zabbix. If not, see <http://www.gnu.org/licenses/>.
import logging
import re


class NullHandler(logging.Handler):
    """Null logger handler.

    :class:`NullHandler` will be used if there are no other logger handlers.
    """

    def emit(self, record):
        pass


class HideSensitiveFilter(logging.Filter):
    """Filter to hide sensitive Zabbix info (password, auth) in logs"""

    def __init__(self, *args, **kwargs):
        super(logging.Filter, self).__init__(*args, **kwargs)
        self.hide_sensitive = HideSensitiveService.hide_sensitive

    def filter(self, record):

        record.msg = self.hide_sensitive(record.msg)
        if record.args:
            newargs = [self.hide_sensitive(arg) if isinstance(arg, str)
                       else arg for arg in record.args]
            record.args = tuple(newargs)

        return 1


class HideSensitiveService(object):
    """
    Service to hide sensitive Zabbix info (password, auth tokens)
    Call classmethod hide_sensitive(message: str)
    """

    HIDEMASK = "********"
    _pattern = re.compile(
        r'(?P<key>password)["\']\s*:\s*u?["\'](?P<password>.+?)["\']'
        r'|'
        r'\W(?P<token>[a-z0-9]{32})')

    @classmethod
    def hide_sensitive(cls, message):
        def hide(m):
            if m.group('key') == 'password':
                return m.string[m.start():m.end()].replace(
                    m.group('password'), cls.HIDEMASK)
            else:
                return m.string[m.start():m.end()].replace(
                    m.group('token'), cls.HIDEMASK)

        message = re.sub(cls._pattern, hide, message)

        return message

sensor.py

"""Support for Zabbix sensors."""
from __future__ import annotations

import logging

import voluptuous as vol

from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType

from .. import zabbix

_LOGGER = logging.getLogger(__name__)

_CONF_TRIGGERS = "triggers"
_CONF_HOSTIDS = "hostids"
_CONF_INDIVIDUAL = "individual"

_ZABBIX_ID_LIST_SCHEMA = vol.Schema([int])
_ZABBIX_TRIGGER_SCHEMA = vol.Schema(
    {
        vol.Optional(_CONF_HOSTIDS, default=[]): _ZABBIX_ID_LIST_SCHEMA,
        vol.Optional(_CONF_INDIVIDUAL, default=False): cv.boolean,
        vol.Optional(CONF_NAME): cv.string,
    }
)

# SCAN_INTERVAL = 30
#
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
    {vol.Required(_CONF_TRIGGERS): vol.Any(_ZABBIX_TRIGGER_SCHEMA, None)}
)


def setup_platform(
    hass: HomeAssistant,
    config: ConfigType,
    add_entities: AddEntitiesCallback,
    discovery_info: DiscoveryInfoType | None = None,
) -> None:
    """Set up the Zabbix sensor platform."""
    sensors: list[ZabbixTriggerCountSensor] = []

    if not (zapi := hass.data[zabbix.DOMAIN]):
        _LOGGER.error("Zabbix integration hasn't been loaded? zapi is None")
        return

    _LOGGER.info("Connected to Zabbix API Version %s", zapi.api_version())

    # The following code seems overly complex. Need to think about this...
    if trigger_conf := config.get(_CONF_TRIGGERS):
        hostids = trigger_conf.get(_CONF_HOSTIDS)
        individual = trigger_conf.get(_CONF_INDIVIDUAL)
        name = trigger_conf.get(CONF_NAME)

        if individual:
            # Individual sensor per host
            if not hostids:
                # We need hostids
                _LOGGER.error("If using 'individual', must specify hostids")
                return

            for hostid in hostids:
                _LOGGER.debug("Creating Zabbix Sensor: %s", str(hostid))
                sensors.append(ZabbixSingleHostTriggerCountSensor(zapi, [hostid], name))
        else:
            if not hostids:
                # Single sensor that provides the total count of triggers.
                _LOGGER.debug("Creating Zabbix Sensor")
                sensors.append(ZabbixTriggerCountSensor(zapi, name))
            else:
                # Single sensor that sums total issues for all hosts
                _LOGGER.debug("Creating Zabbix Sensor group: %s", str(hostids))
                sensors.append(
                    ZabbixMultipleHostTriggerCountSensor(zapi, hostids, name)
                )

    else:
        # Single sensor that provides the total count of triggers.
        _LOGGER.debug("Creating Zabbix Sensor")
        sensors.append(ZabbixTriggerCountSensor(zapi))

    add_entities(sensors)


class ZabbixTriggerCountSensor(SensorEntity):
    """Get the active trigger count for all Zabbix monitored hosts."""

    def __init__(self, zapi, name="Zabbix"):
        """Initialize Zabbix sensor."""
        self._name = name
        self._zapi = zapi
        self._state = None
        self._attributes = {}

    @property
    def name(self):
        """Return the name of the sensor."""
        return self._name

    @property
    def native_value(self):
        """Return the state of the sensor."""
        return self._state

    @property
    def native_unit_of_measurement(self):
        """Return the units of measurement."""
        return "issues"

    def _call_zabbix_api(self):
        return self._zapi.trigger.get(
            output="extend", only_true=1, monitored=1, filter={"value": 1}
        )

    def update(self) -> None:
        """Update the sensor."""
        _LOGGER.debug("Updating ZabbixTriggerCountSensor: %s", str(self._name))
        triggers = self._call_zabbix_api()
        self._state = len(triggers)

    @property
    def extra_state_attributes(self):
        """Return the state attributes of the device."""
        return self._attributes


class ZabbixSingleHostTriggerCountSensor(ZabbixTriggerCountSensor):
    """Get the active trigger count for a single Zabbix monitored host."""

    def __init__(self, zapi, hostid, name=None):
        """Initialize Zabbix sensor."""
        super().__init__(zapi, name)
        self._hostid = hostid
        if not name:
            self._name = self._zapi.host.get(hostids=self._hostid, output="extend")[0][
                "name"
            ]

        self._attributes["Host ID"] = self._hostid

    def _call_zabbix_api(self):
        return self._zapi.trigger.get(
            hostids=self._hostid,
            output="extend",
            only_true=1,
            monitored=1,
            filter={"value": 1},
        )


class ZabbixMultipleHostTriggerCountSensor(ZabbixTriggerCountSensor):
    """Get the active trigger count for specified Zabbix monitored hosts."""

    def __init__(self, zapi, hostids, name=None):
        """Initialize Zabbix sensor."""
        super().__init__(zapi, name)
        self._hostids = hostids
        if not name:
            host_names = self._zapi.host.get(hostids=self._hostids, output="extend")
            self._name = " ".join(name["name"] for name in host_names)
        self._attributes["Host IDs"] = self._hostids

    def _call_zabbix_api(self):
        return self._zapi.trigger.get(
            hostids=self._hostids,
            output="extend",
            only_true=1,
            monitored=1,
            filter={"value": 1},
        )