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

Conversation

ChristianKuehnel
Copy link
Contributor

@ChristianKuehnel ChristianKuehnel commented Dec 19, 2017

Description:

SNMP does not support floating point data as native data types. It rather uses asn1 encoding and transfers them as bytes. The sensor.snmp uses pysnmp and that in turn uses pyasn1 to encode/decode these things. So as long as the encoding is supported by pyasn1, this change should be able to decode it.

Related issue (if applicable): fixes #2767

Pull request in home-assistant.github.io with documentation (if applicable): N/A

Example entry for configuration.yaml (if applicable):

see #2767

Checklist:

If the code does not interact with devices:

  • Local tests with tox run successfully. Your PR cannot be merged unless tests pass
  • Tests have been added to verify that the new code works.

Also added test cases for String, Integer and Real types.
udp.UdpTransport().openServerMode(('127.0.0.1', _PORT))
)
config.addV1System(self.snmpEngine, 'my-area', 'public')
config.addVacmUser(self.snmpEngine, 2, 'my-area', 'noAuthNoPriv', _BASE_OID)

Choose a reason for hiding this comment

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

line too long (84 > 79 characters)

if self.snmpEngine is not None:
self.snmpEngine.transportDispatcher.jobFinished(1)
self.snmpEngine.transportDispatcher.unregisterRecvCbFun(recvId=None)
self.snmpEngine.transportDispatcher.unregisterTransport(udp.domainName)

Choose a reason for hiding this comment

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

line too long (83 > 79 characters)

def tearDown(self):
if self.snmpEngine is not None:
self.snmpEngine.transportDispatcher.jobFinished(1)
self.snmpEngine.transportDispatcher.unregisterRecvCbFun(recvId=None)

Choose a reason for hiding this comment

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

line too long (80 > 79 characters)

@@ -0,0 +1,144 @@
import unittest
import os

Choose a reason for hiding this comment

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

'os' imported but unused

@fanaticDavid
Copy link
Contributor

First of all, I would like to say thank you for giving this a go. Quite honestly, I had lost all hope that someone would ever look into this.

However, I replaced the snmp.py file on my system with your version and after restarting Home Assistant, I got the following error for each of my 4 broken snmp sensors:

2017-12-20 00:42:21 ERROR (MainThread) [homeassistant.components.sensor] Error on device update!
Traceback (most recent call last):
  File "/srv/hass_venv/lib/python3.6/site-packages/homeassistant/helpers/entity_component.py", line 217, in async_add_entity
    yield from entity.async_device_update(warning=False)
  File "/srv/hass_venv/lib/python3.6/site-packages/homeassistant/helpers/entity.py", line 306, in async_device_update
    yield from self.hass.async_add_job(self.update)
  File "/usr/local/lib/python3.6/asyncio/futures.py", line 332, in __iter__
    yield self  # This tells Task to wait for completion.
  File "/usr/local/lib/python3.6/asyncio/tasks.py", line 250, in _wakeup
    future.result()
  File "/usr/local/lib/python3.6/asyncio/futures.py", line 245, in result
    raise self._exception
  File "/usr/local/lib/python3.6/concurrent/futures/thread.py", line 56, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/srv/hass_venv/lib/python3.6/site-packages/homeassistant/components/sensor/snmp.py", line 122, in update
    self.data.update()
  File "/srv/hass_venv/lib/python3.6/site-packages/homeassistant/components/sensor/snmp.py", line 183, in update
    value, _ = decoder.decode(bytes(resrow[-1]))
  File "/srv/hass_venv/lib/python3.6/site-packages/pyasn1/codec/ber/decoder.py", line 1183, in __call__
    '%s not in asn1Spec: %r' % (tagSet, asn1Spec)
pyasn1.error.PyAsn1Error: [128:0:120] not in asn1Spec: None

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

@ChristianKuehnel
Copy link
Contributor Author

Getting closer:

  • you can run a ready-to-use SNMP agent using this docker image: https://hub.docker.com/r/polinux/snmpd/
  • after tinkering with the config file, I now get a float number for the current system load so I have something to test:
09:38 $ snmpget -v2c -c public localhost 1.3.6.1.4.1.2021.10.1.6.1 
iso.3.6.1.4.1.2021.10.1.6.1 = Opaque: Float: 0.787109
  • In the sensor.dnmp this gets me an <class 'pysnmp.proto.rfc1902.Opaque'> with data b'\x9fx\x04>\xde@\x00' with this config:
sensor:
  - platform: snmp
    host: 127.0.0.1
    baseoid: "1.3.6.1.4.1.2021.10.1.6.1"
    name: stringvar
    community: public
    port: 161

logger:
  default: info
  logs:
    homeassistant.components.sensor.snmp: debug

So right now I can successfully decode a Float. Not sure if that is the most elegant solution...

_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

from pysnmp.carrier.asyncore.dgram import udp
from pysnmp.proto.api import v2c
from pyasn1.codec.ber import encoder
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

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

@ChristianKuehnel
Copy link
Contributor Author

I also added a feature request here: etingof/pysnmp#118

@etingof
Copy link

etingof commented Dec 20, 2017

Just a quick note that you may also use pyasn1 infrastructure for reliably decoding whatever ASN.1 structures should occur inside the Opaque SNMP type.

@fanaticDavid
Copy link
Contributor

fanaticDavid commented Dec 23, 2017

I just replaced my snmp.py file once more and I can't believe it...it's working! 😮

Thank you so much, @ChristianKuehnel ! I know this can still be improved when @etingof implements your FR into pysnmp, but this is awesome already in my book! 👍

@ChristianKuehnel
Copy link
Contributor Author

Hey @etingof

if I understand it correctly, the pyasn1 infrastructure does depend on some C/C++ libraries?
If so: I'm concerned about the portability of the code. I guess not all operating systems ship these native libraries...

@etingof
Copy link

etingof commented Dec 23, 2017

if I understand it correctly, the pyasn1 infrastructure does depend on some C/C++ libraries?

Hey, no! pyasn1 is a pure-Python tool and has no external dependencies. pysnmp is totally dependent on pyasn1.

@ChristianKuehnel
Copy link
Contributor Author

OK good.
Looking at the library, I did not see support for a "Float" data type. Did I miss something?

@etingof
Copy link

etingof commented Dec 24, 2017

Right, there is no Float type in ASN.1 because they borrowed it from the IEEE standard. So unpacking float itself look good to me.

What I meant though is that the wrapping BER encoding could be handled by pyasn1 like this:

floatType = OctetString().subtype(
    implicitTag=Tag(tagClassContext, tagFormatSimple, 120),
    subtypeSpec=ValueSizeConstraint(4, 4)
)
ieee_encoded_float, _ = decode(b'\x9f\x78\x04\x00\x00\x00\x00', asn1Spec=floatType)

@fanaticDavid
Copy link
Contributor

fanaticDavid commented Jan 1, 2018

Hmmm I can't seem to apply a value_template to Opaque types.

I have the following SNMP sensors (the bottom 4 are Opaque types):

schermafbeelding 2018-01-01 om 04 30 02

I wish to change the shown state for the battery voltage sensor (for obvious reasons) using the following: value_template: '{{ (value | float) | round(1) }}'.
After restarting Home Assistant, this results in the following error in the log file:

2018-01-01 04:34:02 ERROR (MainThread) [homeassistant.components.sensor] Error on device update!
Traceback (most recent call last):
  File "/srv/hass_venv/lib/python3.6/site-packages/homeassistant/helpers/entity_component.py", line 217, in async_add_entity
    yield from entity.async_device_update(warning=False)
  File "/srv/hass_venv/lib/python3.6/site-packages/homeassistant/helpers/entity.py", line 306, in async_device_update
    yield from self.hass.async_add_job(self.update)
  File "/usr/local/lib/python3.6/asyncio/futures.py", line 332, in __iter__
    yield self  # This tells Task to wait for completion.
  File "/usr/local/lib/python3.6/asyncio/tasks.py", line 250, in _wakeup
    future.result()
  File "/usr/local/lib/python3.6/asyncio/futures.py", line 245, in result
    raise self._exception
  File "/usr/local/lib/python3.6/concurrent/futures/thread.py", line 56, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/srv/hass_venv/lib/python3.6/site-packages/homeassistant/components/sensor/snmp.py", line 131, in update
    value, STATE_UNKNOWN)
  File "/srv/hass_venv/lib/python3.6/site-packages/homeassistant/helpers/template.py", line 129, in render_with_possible_json_value
    error_value).result()
  File "/usr/local/lib/python3.6/concurrent/futures/_base.py", line 432, in result
    return self.__get_result()
  File "/usr/local/lib/python3.6/concurrent/futures/_base.py", line 384, in __get_result
    raise self._exception
  File "/srv/hass_venv/lib/python3.6/site-packages/homeassistant/util/async.py", line 167, in run_callback
    future.set_result(callback(*args))
  File "/srv/hass_venv/lib/python3.6/site-packages/homeassistant/helpers/template.py", line 147, in async_render_with_possible_json_value
    variables['value_json'] = json.loads(value)
  File "/usr/local/lib/python3.6/json/__init__.py", line 348, in loads
    'not {!r}'.format(s.__class__.__name__))
TypeError: the JSON object must be str, bytes or bytearray, not 'float'

I know my template is fine as I'm able to use it with other sensors (e.g. InfluxDB sensors).

@ChristianKuehnel
Copy link
Contributor Author

Hmm, OK. I added a type cast to string before returning the float. Can you give it another shot?

@fanaticDavid
Copy link
Contributor

@ChristianKuehnel Just tested your change, it works! 😀

@ChristianKuehnel
Copy link
Contributor Author

ChristianKuehnel commented Jan 1, 2018

weird test results --> closing/opening to trigger new build.
Here the build was successful: https://travis-ci.org/ChristianKuehnel/home-assistant/builds/323770286

@ChristianKuehnel
Copy link
Contributor Author

This is weird. The error on Travis is:

requirements_test_all.txt is not up to date
Please run script/gen_requirements_all.py
ERROR: InvocationError: '/home/travis/build/home-assistant/home-assistant/.tox/lint/bin/python script/gen_requirements_all.py validate'
  • If I run the check locally, it passes.
  • If I run script/gen_requirements_all.py, there is not change on the file system, I cannot commit anything...
    --> not sure how to fix this.

@ChristianKuehnel
Copy link
Contributor Author

problem resolved with a merge of Dev and a re-run of gen_requirements_all

@fanaticDavid
Copy link
Contributor

Should this have been included in 0.61.0? I've been using the code in this PR for the past 2 weeks without issues 😁

@ChristianKuehnel
Copy link
Contributor Author

As the change was not yet merged, it's not contained in the 0.61 release.

It seems with my latest merge of the Dev branch I broke some tests. I'll have a investigate this on the weekend...

decoded_value, _ = decoder.decode(bytes(value))
return str(decoded_value)
# pylint: disable=broad-except
except Exception as decode_exception:
Copy link
Member

Choose a reason for hiding this comment

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

Isn't it possible to catch a more specific error, like ValueError?

# pylint: disable=broad-except
except Exception as decode_exception:
_LOGGER.error('SNMP error in decoding opaque type: %s',
str(decode_exception))
Copy link
Member

Choose a reason for hiding this comment

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

Copy to string shouldn't be necessary.

"""Test basic functionality of sensor.snmp."""

def setUp(self):
"""Setup a hoss instance for testing."""
Copy link
Member

Choose a reason for hiding this comment

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

Typo.

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.

@balloob
Copy link
Member

balloob commented Apr 9, 2018

what's the status?

@pvizeli
Copy link
Member

pvizeli commented Apr 17, 2018

Please reopen the PR after all comments are addressed

@pvizeli pvizeli closed this Apr 17, 2018
@home-assistant home-assistant locked and limited conversation to collaborators Jul 26, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

SNMP sensor not processing float values (correctly)
10 participants