Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for displaying collected data over SNMP."""
2 
3 from __future__ import annotations
4 
5 from datetime import timedelta
6 import logging
7 from struct import unpack
8 
9 from pyasn1.codec.ber import decoder
10 from pysnmp.error import PySnmpError
11 import pysnmp.hlapi.asyncio as hlapi
12 from pysnmp.hlapi.asyncio import (
13  CommunityData,
14  Udp6TransportTarget,
15  UdpTransportTarget,
16  UsmUserData,
17  getCmd,
18 )
19 from pysnmp.proto.rfc1902 import Opaque
20 from pysnmp.proto.rfc1905 import NoSuchObject
21 import voluptuous as vol
22 
24  CONF_STATE_CLASS,
25  PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
26 )
27 from homeassistant.const import (
28  CONF_DEVICE_CLASS,
29  CONF_HOST,
30  CONF_ICON,
31  CONF_NAME,
32  CONF_PORT,
33  CONF_UNIQUE_ID,
34  CONF_UNIT_OF_MEASUREMENT,
35  CONF_USERNAME,
36  CONF_VALUE_TEMPLATE,
37  STATE_UNKNOWN,
38 )
39 from homeassistant.core import HomeAssistant
41 from homeassistant.helpers.entity_platform import AddEntitiesCallback
42 from homeassistant.helpers.template import Template
44  CONF_AVAILABILITY,
45  CONF_PICTURE,
46  TEMPLATE_SENSOR_BASE_SCHEMA,
47  ManualTriggerSensorEntity,
48 )
49 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
50 
51 from .const import (
52  CONF_ACCEPT_ERRORS,
53  CONF_AUTH_KEY,
54  CONF_AUTH_PROTOCOL,
55  CONF_BASEOID,
56  CONF_COMMUNITY,
57  CONF_DEFAULT_VALUE,
58  CONF_PRIV_KEY,
59  CONF_PRIV_PROTOCOL,
60  CONF_VERSION,
61  DEFAULT_AUTH_PROTOCOL,
62  DEFAULT_COMMUNITY,
63  DEFAULT_HOST,
64  DEFAULT_NAME,
65  DEFAULT_PORT,
66  DEFAULT_PRIV_PROTOCOL,
67  DEFAULT_TIMEOUT,
68  DEFAULT_VERSION,
69  MAP_AUTH_PROTOCOLS,
70  MAP_PRIV_PROTOCOLS,
71  SNMP_VERSIONS,
72 )
73 from .util import async_create_request_cmd_args
74 
75 _LOGGER = logging.getLogger(__name__)
76 
77 SCAN_INTERVAL = timedelta(seconds=10)
78 
79 TRIGGER_ENTITY_OPTIONS = (
80  CONF_AVAILABILITY,
81  CONF_DEVICE_CLASS,
82  CONF_ICON,
83  CONF_PICTURE,
84  CONF_UNIQUE_ID,
85  CONF_STATE_CLASS,
86  CONF_UNIT_OF_MEASUREMENT,
87 )
88 
89 PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
90  {
91  vol.Required(CONF_BASEOID): cv.string,
92  vol.Optional(CONF_ACCEPT_ERRORS, default=False): cv.boolean,
93  vol.Optional(CONF_COMMUNITY, default=DEFAULT_COMMUNITY): cv.string,
94  vol.Optional(CONF_DEFAULT_VALUE): cv.string,
95  vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
96  vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
97  vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
98  vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): vol.In(SNMP_VERSIONS),
99  vol.Optional(CONF_USERNAME): cv.string,
100  vol.Optional(CONF_AUTH_KEY): cv.string,
101  vol.Optional(CONF_AUTH_PROTOCOL, default=DEFAULT_AUTH_PROTOCOL): vol.In(
102  MAP_AUTH_PROTOCOLS
103  ),
104  vol.Optional(CONF_PRIV_KEY): cv.string,
105  vol.Optional(CONF_PRIV_PROTOCOL, default=DEFAULT_PRIV_PROTOCOL): vol.In(
106  MAP_PRIV_PROTOCOLS
107  ),
108  }
109 ).extend(TEMPLATE_SENSOR_BASE_SCHEMA.schema)
110 
111 
113  hass: HomeAssistant,
114  config: ConfigType,
115  async_add_entities: AddEntitiesCallback,
116  discovery_info: DiscoveryInfoType | None = None,
117 ) -> None:
118  """Set up the SNMP sensor."""
119  host = config.get(CONF_HOST)
120  port = config.get(CONF_PORT)
121  community = config.get(CONF_COMMUNITY)
122  baseoid: str = config[CONF_BASEOID]
123  version = config[CONF_VERSION]
124  username = config.get(CONF_USERNAME)
125  authkey = config.get(CONF_AUTH_KEY)
126  authproto = config[CONF_AUTH_PROTOCOL]
127  privkey = config.get(CONF_PRIV_KEY)
128  privproto = config[CONF_PRIV_PROTOCOL]
129  accept_errors = config.get(CONF_ACCEPT_ERRORS)
130  default_value = config.get(CONF_DEFAULT_VALUE)
131 
132  try:
133  # Try IPv4 first.
134  target = UdpTransportTarget((host, port), timeout=DEFAULT_TIMEOUT)
135  except PySnmpError:
136  # Then try IPv6.
137  try:
138  target = Udp6TransportTarget((host, port), timeout=DEFAULT_TIMEOUT)
139  except PySnmpError as err:
140  _LOGGER.error("Invalid SNMP host: %s", err)
141  return
142 
143  if version == "3":
144  if not authkey:
145  authproto = "none"
146  if not privkey:
147  privproto = "none"
148  auth_data = UsmUserData(
149  username,
150  authKey=authkey or None,
151  privKey=privkey or None,
152  authProtocol=getattr(hlapi, MAP_AUTH_PROTOCOLS[authproto]),
153  privProtocol=getattr(hlapi, MAP_PRIV_PROTOCOLS[privproto]),
154  )
155  else:
156  auth_data = CommunityData(community, mpModel=SNMP_VERSIONS[version])
157 
158  request_args = await async_create_request_cmd_args(hass, auth_data, target, baseoid)
159  get_result = await getCmd(*request_args)
160  errindication, _, _, _ = get_result
161 
162  if errindication and not accept_errors:
163  _LOGGER.error(
164  "Please check the details in the configuration file: %s",
165  errindication,
166  )
167  return
168 
169  name = config.get(CONF_NAME, Template(DEFAULT_NAME, hass))
170  trigger_entity_config = {CONF_NAME: name}
171  for key in TRIGGER_ENTITY_OPTIONS:
172  if key not in config:
173  continue
174  trigger_entity_config[key] = config[key]
175 
176  value_template: Template | None = config.get(CONF_VALUE_TEMPLATE)
177 
178  data = SnmpData(request_args, baseoid, accept_errors, default_value)
179  async_add_entities([SnmpSensor(hass, data, trigger_entity_config, value_template)])
180 
181 
183  """Representation of a SNMP sensor."""
184 
185  _attr_should_poll = True
186 
187  def __init__(
188  self,
189  hass: HomeAssistant,
190  data: SnmpData,
191  config: ConfigType,
192  value_template: Template | None,
193  ) -> None:
194  """Initialize the sensor."""
195  super().__init__(hass, config)
196  self.datadata = data
197  self._state_state = None
198  self._value_template_value_template = value_template
199 
200  async def async_added_to_hass(self) -> None:
201  """Handle adding to Home Assistant."""
202  await super().async_added_to_hass()
203  await self.async_updateasync_update()
204 
205  async def async_update(self) -> None:
206  """Get the latest data and updates the states."""
207  await self.datadata.async_update()
208 
209  raw_value = self.datadata.value
210 
211  if (value := self.datadata.value) is None:
212  value = STATE_UNKNOWN
213  elif self._value_template_value_template is not None:
214  value = self._value_template_value_template.async_render_with_possible_json_value(
215  value, STATE_UNKNOWN
216  )
217 
218  self._attr_native_value_attr_native_value = value
219  self._process_manual_data_process_manual_data(raw_value)
220 
221 
222 class SnmpData:
223  """Get the latest data and update the states."""
224 
225  def __init__(self, request_args, baseoid, accept_errors, default_value) -> None:
226  """Initialize the data object."""
227  self._request_args_request_args = request_args
228  self._baseoid_baseoid = baseoid
229  self._accept_errors_accept_errors = accept_errors
230  self._default_value_default_value = default_value
231  self.valuevalue = None
232 
233  async def async_update(self):
234  """Get the latest data from the remote SNMP capable host."""
235 
236  get_result = await getCmd(*self._request_args_request_args)
237  errindication, errstatus, errindex, restable = get_result
238 
239  if errindication and not self._accept_errors_accept_errors:
240  _LOGGER.error("SNMP error: %s", errindication)
241  elif errstatus and not self._accept_errors_accept_errors:
242  _LOGGER.error(
243  "SNMP error: %s at %s",
244  errstatus.prettyPrint(),
245  restable[-1][int(errindex) - 1] if errindex else "?",
246  )
247  elif (errindication or errstatus) and self._accept_errors_accept_errors:
248  self.valuevalue = self._default_value_default_value
249  else:
250  for resrow in restable:
251  self.valuevalue = self._decode_value_decode_value(resrow[-1])
252 
253  def _decode_value(self, value):
254  """Decode the different results we could get into strings."""
255 
256  _LOGGER.debug(
257  "SNMP OID %s received type=%s and data %s",
258  self._baseoid_baseoid,
259  type(value),
260  value,
261  )
262  if isinstance(value, NoSuchObject):
263  _LOGGER.error(
264  "SNMP error for OID %s: No Such Object currently exists at this OID",
265  self._baseoid_baseoid,
266  )
267  return self._default_value_default_value
268 
269  if isinstance(value, Opaque):
270  # Float data type is not supported by the pyasn1 library,
271  # so we need to decode this type ourselves based on:
272  # https://tools.ietf.org/html/draft-perkins-opaque-01
273  if bytes(value).startswith(b"\x9f\x78"):
274  return str(unpack("!f", bytes(value)[3:])[0])
275  # Otherwise Opaque types should be asn1 encoded
276  try:
277  decoded_value, _ = decoder.decode(bytes(value))
278  return str(decoded_value)
279  except Exception as decode_exception: # noqa: BLE001
280  _LOGGER.error(
281  "SNMP error in decoding opaque type: %s", decode_exception
282  )
283  return self._default_value_default_value
284  return str(value)
None __init__(self, request_args, baseoid, accept_errors, default_value)
Definition: sensor.py:225
None __init__(self, HomeAssistant hass, SnmpData data, ConfigType config, Template|None value_template)
Definition: sensor.py:193
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: sensor.py:117
RequestArgsType async_create_request_cmd_args(HomeAssistant hass, UsmUserData|CommunityData auth_data, UdpTransportTarget|Udp6TransportTarget target, str object_id)
Definition: util.py:63