snmp: Better sensor support to resolve previous issues (#113624)

Co-authored-by: Christian Kühnel <christian.kuehnel@gmail.com>
Co-authored-by: jan iversen <jancasacondor@gmail.com>
This commit is contained in:
Lex Li 2024-03-16 17:56:21 -04:00 committed by GitHub
parent 86ccb99f4c
commit 2bc4a5067d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 201 additions and 8 deletions

View file

@ -4,7 +4,9 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import logging import logging
from struct import unpack
from pyasn1.codec.ber import decoder
from pysnmp.error import PySnmpError from pysnmp.error import PySnmpError
import pysnmp.hlapi.asyncio as hlapi import pysnmp.hlapi.asyncio as hlapi
from pysnmp.hlapi.asyncio import ( from pysnmp.hlapi.asyncio import (
@ -18,6 +20,8 @@ from pysnmp.hlapi.asyncio import (
UsmUserData, UsmUserData,
getCmd, getCmd,
) )
from pysnmp.proto.rfc1902 import Opaque
from pysnmp.proto.rfc1905 import NoSuchObject
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import CONF_STATE_CLASS, PLATFORM_SCHEMA from homeassistant.components.sensor import CONF_STATE_CLASS, PLATFORM_SCHEMA
@ -165,7 +169,10 @@ async def async_setup_platform(
errindication, _, _, _ = get_result errindication, _, _, _ = get_result
if errindication and not accept_errors: if errindication and not accept_errors:
_LOGGER.error("Please check the details in the configuration file") _LOGGER.error(
"Please check the details in the configuration file: %s",
errindication,
)
return return
name = config.get(CONF_NAME, Template(DEFAULT_NAME, hass)) name = config.get(CONF_NAME, Template(DEFAULT_NAME, hass))
@ -248,10 +255,44 @@ class SnmpData:
_LOGGER.error( _LOGGER.error(
"SNMP error: %s at %s", "SNMP error: %s at %s",
errstatus.prettyPrint(), errstatus.prettyPrint(),
errindex and restable[-1][int(errindex) - 1] or "?", restable[-1][int(errindex) - 1] if errindex else "?",
) )
elif (errindication or errstatus) and self._accept_errors: elif (errindication or errstatus) and self._accept_errors:
self.value = self._default_value self.value = self._default_value
else: else:
for resrow in restable: for resrow in restable:
self.value = resrow[-1].prettyPrint() self.value = self._decode_value(resrow[-1])
def _decode_value(self, value):
"""Decode the different results we could get into strings."""
_LOGGER.debug(
"SNMP OID %s received type=%s and data %s",
self._baseoid,
type(value),
bytes(value),
)
if isinstance(value, NoSuchObject):
_LOGGER.error(
"SNMP error for OID %s: No Such Object currently exists at this OID",
self._baseoid,
)
return self._default_value
if isinstance(value, Opaque):
# Float data type is not supported by the pyasn1 library,
# so we need to decode this type ourselves based on:
# https://tools.ietf.org/html/draft-perkins-opaque-01
if bytes(value).startswith(b"\x9f\x78"):
return str(unpack("!f", bytes(value)[3:])[0])
# Otherwise Opaque types should be asn1 encoded
try:
decoded_value, _ = decoder.decode(bytes(value))
return str(decoded_value)
# pylint: disable=broad-except
except Exception as decode_exception:
_LOGGER.error(
"SNMP error in decoding opaque type: %s", decode_exception
)
return self._default_value
return str(value)

View file

@ -0,0 +1,79 @@
"""SNMP sensor tests."""
from unittest.mock import patch
from pysnmp.proto.rfc1902 import Opaque
import pytest
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
@pytest.fixture(autouse=True)
def hlapi_mock():
"""Mock out 3rd party API."""
mock_data = Opaque(value=b"\x9fx\x04=\xa4\x00\x00")
with patch(
"homeassistant.components.snmp.sensor.getCmd",
return_value=(None, None, None, [[mock_data]]),
):
yield
async def test_basic_config(hass: HomeAssistant) -> None:
"""Test basic entity configuration."""
config = {
SENSOR_DOMAIN: {
"platform": "snmp",
"host": "192.168.1.32",
"baseoid": "1.3.6.1.4.1.2021.10.1.3.1",
},
}
assert await async_setup_component(hass, SENSOR_DOMAIN, config)
await hass.async_block_till_done()
state = hass.states.get("sensor.snmp")
assert state.state == "0.080078125"
assert state.attributes == {"friendly_name": "SNMP"}
async def test_entity_config(hass: HomeAssistant) -> None:
"""Test entity configuration."""
config = {
SENSOR_DOMAIN: {
# SNMP configuration
"platform": "snmp",
"host": "192.168.1.32",
"baseoid": "1.3.6.1.4.1.2021.10.1.3.1",
# Entity configuration
"icon": "{{'mdi:one_two_three'}}",
"picture": "{{'blabla.png'}}",
"device_class": "temperature",
"name": "{{'SNMP' + ' ' + 'Sensor'}}",
"state_class": "measurement",
"unique_id": "very_unique",
"unit_of_measurement": "°C",
},
}
assert await async_setup_component(hass, SENSOR_DOMAIN, config)
await hass.async_block_till_done()
entity_registry = er.async_get(hass)
assert entity_registry.async_get("sensor.snmp_sensor").unique_id == "very_unique"
state = hass.states.get("sensor.snmp_sensor")
assert state.state == "0.080078125"
assert state.attributes == {
"device_class": "temperature",
"entity_picture": "blabla.png",
"friendly_name": "SNMP Sensor",
"icon": "mdi:one_two_three",
"state_class": "measurement",
"unit_of_measurement": "°C",
}

View file

@ -1,7 +1,8 @@
"""SNMP sensor tests.""" """SNMP sensor tests."""
from unittest.mock import MagicMock, Mock, patch from unittest.mock import patch
from pysnmp.hlapi import Integer32
import pytest import pytest
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
@ -13,8 +14,7 @@ from homeassistant.setup import async_setup_component
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def hlapi_mock(): def hlapi_mock():
"""Mock out 3rd party API.""" """Mock out 3rd party API."""
mock_data = MagicMock() mock_data = Integer32(13)
mock_data.prettyPrint = Mock(return_value="13.5")
with patch( with patch(
"homeassistant.components.snmp.sensor.getCmd", "homeassistant.components.snmp.sensor.getCmd",
return_value=(None, None, None, [[mock_data]]), return_value=(None, None, None, [[mock_data]]),
@ -37,7 +37,7 @@ async def test_basic_config(hass: HomeAssistant) -> None:
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("sensor.snmp") state = hass.states.get("sensor.snmp")
assert state.state == "13.5" assert state.state == "13"
assert state.attributes == {"friendly_name": "SNMP"} assert state.attributes == {"friendly_name": "SNMP"}
@ -68,7 +68,7 @@ async def test_entity_config(hass: HomeAssistant) -> None:
assert entity_registry.async_get("sensor.snmp_sensor").unique_id == "very_unique" assert entity_registry.async_get("sensor.snmp_sensor").unique_id == "very_unique"
state = hass.states.get("sensor.snmp_sensor") state = hass.states.get("sensor.snmp_sensor")
assert state.state == "13.5" assert state.state == "13"
assert state.attributes == { assert state.attributes == {
"device_class": "temperature", "device_class": "temperature",
"entity_picture": "blabla.png", "entity_picture": "blabla.png",

View file

@ -0,0 +1,73 @@
"""SNMP sensor tests."""
from unittest.mock import patch
from pysnmp.proto.rfc1902 import OctetString
import pytest
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
@pytest.fixture(autouse=True)
def hlapi_mock():
"""Mock out 3rd party API."""
mock_data = OctetString("98F")
with patch(
"homeassistant.components.snmp.sensor.getCmd",
return_value=(None, None, None, [[mock_data]]),
):
yield
async def test_basic_config(hass: HomeAssistant) -> None:
"""Test basic entity configuration."""
config = {
SENSOR_DOMAIN: {
"platform": "snmp",
"host": "192.168.1.32",
"baseoid": "1.3.6.1.4.1.2021.10.1.3.1",
},
}
assert await async_setup_component(hass, SENSOR_DOMAIN, config)
await hass.async_block_till_done()
state = hass.states.get("sensor.snmp")
assert state.state == "98F"
assert state.attributes == {"friendly_name": "SNMP"}
async def test_entity_config(hass: HomeAssistant) -> None:
"""Test entity configuration."""
config = {
SENSOR_DOMAIN: {
# SNMP configuration
"platform": "snmp",
"host": "192.168.1.32",
"baseoid": "1.3.6.1.4.1.2021.10.1.3.1",
# Entity configuration
"icon": "{{'mdi:one_two_three'}}",
"picture": "{{'blabla.png'}}",
"name": "{{'SNMP' + ' ' + 'Sensor'}}",
"unique_id": "very_unique",
},
}
assert await async_setup_component(hass, SENSOR_DOMAIN, config)
await hass.async_block_till_done()
entity_registry = er.async_get(hass)
assert entity_registry.async_get("sensor.snmp_sensor").unique_id == "very_unique"
state = hass.states.get("sensor.snmp_sensor")
assert state.state == "98F"
assert state.attributes == {
"entity_picture": "blabla.png",
"friendly_name": "SNMP Sensor",
"icon": "mdi:one_two_three",
}