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

sensor.snmp added decoding of "Opaque" types. #11239

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 28 additions & 2 deletions homeassistant/components/sensor/snmp.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
ObjectType(ObjectIdentity(baseoid))))

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 False
else:
data = SnmpData(
Expand Down Expand Up @@ -151,6 +152,7 @@ def update(self):
from pysnmp.hlapi import (
getCmd, CommunityData, SnmpEngine, UdpTransportTarget, ContextData,
ObjectType, ObjectIdentity)

errindication, errstatus, errindex, restable = next(
getCmd(SnmpEngine(),
CommunityData(self._community, mpModel=self._version),
Expand All @@ -168,4 +170,28 @@ def update(self):
self.value = self._default_value
else:
for resrow in restable:
self.value = str(resrow[-1])
self.value = self._decode_value(resrow[-1])

def _decode_value(self, value):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring is missing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

from pysnmp.proto.rfc1905 import NoSuchObject
from pysnmp.proto.rfc1902 import Opaque
from pyasn1.codec.ber import decoder

_LOGGER.debug("SNMP OID %s received type=%s and data %s",
self._baseoid, type(value), bytes(value))
if type(value) == NoSuchObject:
_LOGGER.error(
"SNMP error for OID %s: "
"No Such Object currently exists at this OID",
self._baseoid)
return self._default_value

if type(value) == Opaque:
try:
decoded_value, _ = decoder.decode(bytes(value))
return str(decoded_value)
except Exception as e:
_LOGGER.error('SNMP error in decoding opaque type: %s',
str(e))
return self._default_value
return str(value)
3 changes: 3 additions & 0 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,6 @@ warrant==0.6.1

# homeassistant.components.sensor.yahoo_finance
yahoo-finance==1.4.0

# homeassistant.components.sensor.snmp
pysnmp==4.4.2
146 changes: 146 additions & 0 deletions tests/components/sensor/test_snmp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import unittest
from homeassistant.setup import setup_component
from tests.common import get_test_home_assistant
from threading import Thread

from pysnmp.entity import engine, config
from pysnmp.entity.rfc3413 import cmdrsp, context
from pysnmp.carrier.asyncore.dgram import udp
from pysnmp.proto.api import v2c
from pyasn1.codec.ber import encoder

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'pyasn1.codec.ber.encoder' imported but unused

from pyasn1.type import univ

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'pyasn1.type.univ' imported but unused


import time

_PORT = 10161
_BASE_OID = (1, 3, 6, 1, 4, 1, 6574, 4)
_OID1 = _BASE_OID + (1, 1, 1)
_OID2 = _BASE_OID + (2, 1, 1)
_OID3 = _BASE_OID + (3, 1, 1)


class TestSnmp(unittest.TestCase):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add docstrings to the whole file. Thanks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


def setUp(self):
self.hass = get_test_home_assistant()
self.hass.start()
self.snmpEngine = None
self._agent_thread = None

def tearDown(self):
if self.snmpEngine is not None:
self.snmpEngine.transportDispatcher.jobFinished(1)
self.snmpEngine.transportDispatcher.unregisterRecvCbFun(
recvId=None)
self.snmpEngine.transportDispatcher.unregisterTransport(
udp.domainName)
if self._agent_thread is not None:
self._agent_thread.join(0.01)
self.hass.stop()

def _run_agent(self):
self.snmpEngine = engine.SnmpEngine()
config.addTransport(
self.snmpEngine,
udp.domainName,
udp.UdpTransport().openServerMode(('127.0.0.1', _PORT))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the engine should be mocked. Why do we need a real engine? We only need to test the home assistant side of the logic.

)
config.addV1System(self.snmpEngine, 'my-area', 'public')
config.addVacmUser(self.snmpEngine, 2, 'my-area', 'noAuthNoPriv',
_BASE_OID)
snmpContext = context.SnmpContext(self.snmpEngine)
mibBuilder = snmpContext.getMibInstrum().getMibBuilder()

MibScalar, MibScalarInstance = mibBuilder.importSymbols(
'SNMPv2-SMI', 'MibScalar', 'MibScalarInstance'
)

class MyStaticMibScalarInstance1(MibScalarInstance):
# noinspection PyUnusedLocal,PyUnusedLocal
def getValue(self, name, idx):
return self.getSyntax().clone("test string")

class MyStaticMibScalarInstance2(MibScalarInstance):
# noinspection PyUnusedLocal,PyUnusedLocal
def getValue(self, name, idx):
return self.getSyntax().clone(1234)

class MyStaticMibScalarInstance3(MibScalarInstance):
# noinspection PyUnusedLocal,PyUnusedLocal
def getValue(self, name, idx):
encoded_value = encoder.encode(univ.Real(1.17))
return self.getSyntax().clone(encoded_value)

mibBuilder.exportSymbols(
'__MY_MIB', MibScalar(_OID1, v2c.OctetString()),
MyStaticMibScalarInstance1(_OID1, (0,), v2c.OctetString())
)

mibBuilder.exportSymbols(
'__MY_MIB', MibScalar(_OID2, v2c.Integer()),
MyStaticMibScalarInstance2(_OID2, (0,), v2c.Integer())
)

mibBuilder.exportSymbols(
'__MY_MIB', MibScalar(_OID3, v2c.Opaque()),
MyStaticMibScalarInstance3(_OID3, (0,), v2c.Opaque())
)

cmdrsp.GetCommandResponder(self.snmpEngine, snmpContext)
cmdrsp.NextCommandResponder(self.snmpEngine, snmpContext)
cmdrsp.BulkCommandResponder(self.snmpEngine, snmpContext)
self.snmpEngine.transportDispatcher.jobStarted(1)

# Run I/O dispatcher which would receive queries and send responses
try:
self.snmpEngine.transportDispatcher.runDispatcher()
except:
self.snmpEngine.transportDispatcher.closeDispatcher()
raise

def test_read_values(self):
self._agent_thread = Thread(target=self._run_agent)
self._agent_thread.start()
time.sleep(1)
assert setup_component(self.hass, 'sensor', {
'sensor': [
{
'platform': 'snmp',
'host': '127.0.0.1',
'baseoid': '.'.join([str(i) for i in _OID1])+'.0',
'name': 'stringvar',
'community': 'public',
'port': _PORT,
'accept_errors': False,
'version': '2c',
},
{
'platform': 'snmp',
'host': '127.0.0.1',
'baseoid': '.'.join([str(i) for i in _OID2])+'.0',
'name': 'intvar',
'community': 'public',
'port': _PORT,
'accept_errors': False,
'version': '2c',
},
{
'platform': 'snmp',
'host': '127.0.0.1',
'baseoid': '.'.join([str(i) for i in _OID3]) + '.0',
'name': 'floatvar',
'community': 'public',
'port': _PORT,
'accept_errors': False,
'version': '2c',
},
]
})
str_state = self.hass.states.get('sensor.stringvar')
self.assertEquals('test string', str_state.state)

int_state = self.hass.states.get('sensor.intvar')
self.assertEquals('1234', int_state.state)

int_state = self.hass.states.get('sensor.floatvar')
self.assertEquals('1.17', int_state.state)