Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

snmp: Better sensor support to resolve previous issues #113624

Merged
merged 12 commits into from
Mar 16, 2024
47 changes: 44 additions & 3 deletions homeassistant/components/snmp/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

from datetime import timedelta
import logging
from struct import unpack

from pyasn1.codec.ber import decoder
from pysnmp.error import PySnmpError
import pysnmp.hlapi.asyncio as hlapi
from pysnmp.hlapi.asyncio import (
Expand All @@ -18,6 +20,8 @@
UsmUserData,
getCmd,
)
from pysnmp.proto.rfc1902 import Opaque
from pysnmp.proto.rfc1905 import NoSuchObject
import voluptuous as vol

from homeassistant.components.sensor import CONF_STATE_CLASS, PLATFORM_SCHEMA
Expand Down Expand Up @@ -165,7 +169,10 @@ async def async_setup_platform(
errindication, _, _, _ = get_result

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

name = config.get(CONF_NAME, Template(DEFAULT_NAME, hass))
Expand Down Expand Up @@ -248,10 +255,44 @@ async def async_update(self):
_LOGGER.error(
"SNMP error: %s at %s",
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:
self.value = self._default_value
else:
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)
79 changes: 79 additions & 0 deletions tests/components/snmp/test_float_sensor.py
Original file line number Diff line number Diff line change
@@ -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",
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""SNMP sensor tests."""

from unittest.mock import MagicMock, Mock, patch
from unittest.mock import patch

from pysnmp.hlapi import Integer32
import pytest

from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
Expand All @@ -13,8 +14,7 @@
@pytest.fixture(autouse=True)
def hlapi_mock():
"""Mock out 3rd party API."""
mock_data = MagicMock()
mock_data.prettyPrint = Mock(return_value="13.5")
mock_data = Integer32(13)
with patch(
"homeassistant.components.snmp.sensor.getCmd",
return_value=(None, None, None, [[mock_data]]),
Expand All @@ -37,7 +37,7 @@ async def test_basic_config(hass: HomeAssistant) -> None:
await hass.async_block_till_done()

state = hass.states.get("sensor.snmp")
assert state.state == "13.5"
assert state.state == "13"
assert state.attributes == {"friendly_name": "SNMP"}


Expand Down Expand Up @@ -68,7 +68,7 @@ async def test_entity_config(hass: HomeAssistant) -> None:
assert entity_registry.async_get("sensor.snmp_sensor").unique_id == "very_unique"

state = hass.states.get("sensor.snmp_sensor")
assert state.state == "13.5"
assert state.state == "13"
assert state.attributes == {
"device_class": "temperature",
"entity_picture": "blabla.png",
Expand Down
73 changes: 73 additions & 0 deletions tests/components/snmp/test_string_sensor.py
Original file line number Diff line number Diff line change
@@ -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",
}