Source code for neat.translator.obvius

#!/usr/bin/env python
# -*- encoding: utf-8 -*-
#
# Copyright (c) 2017 Stephen Bunn (stephen@bunn.io)
# GNU GPLv3 <https://www.gnu.org/licenses/gpl-3.0.en.html>


import time
import importlib

from .. import const, device
from ..models import Record, RecordPoint
from ._common import AbstractTranslator

import bs4
import pint
import dateutil.parser


[docs]class ObviusTranslator(AbstractTranslator): """ The translator for Obvius devices. """ supported_requesters = ('ObviusRequester',) _parser_pref = ['lxml', 'html.parser'] _expression_unit_map = { # energy 'kWh': 'kilowatthour', 'Wh': 'watthour', 'MWh': 'megawatthour', 'VAh': 'volt_ampere * hour', 'kVAh': 'kilovolt_ampere * hour', 'MVAh': 'megavolt_ampere * hour', 'VARh': 'volt_ampere * hour', 'kVARh': 'kilovolt_ampere * hour', 'MVARh': 'megavolt_ampere * hour', 'Btu': 'btu', 'kBtu': 'kilobtu', 'MBtu': 'megabtu', 'MMBtu': 'megabtu', 'BtuE6': 'megabtu', 'Ton-Hrs': 'ton * hour', 'therms': 'thm', # power 'W': 'watt', 'mW': 'milliwatt', 'kW': 'kilowatt', 'MW': 'megawatt', 'VA': 'volt_ampere', 'kVA': 'kilovolt_ampere', 'MVA': 'megavolt_ampere', 'VAR': 'volt_ampere', 'kVAR': 'kilovolt_ampere', 'MVAR': 'megavolt_ampere', 'Btu/hr': 'btu/hr', # voltage 'Volts': 'volt', 'mV': 'millivolt', 'kV': 'kilovolt', 'MV': 'megavolt', # current 'Amps': 'amp', 'mA': 'milliamp', # event counting 'cycles': 'cycles', 'pulses': '', 'revolutions': 'revolution', 'starts': '', # frequency 'Hz': 'hertz', 'kHz': 'kilohertz', 'RPM': 'rpm', # resistance 'Ohms': 'ohm', 'kohms': 'kiloohm', # mass 'kgs': 'kilogram', 'Lbs': 'pound', 'Tons': 'ton', # mass flow 'kg/hr': 'kilogram/hour', 'Lb/hr': 'pound/hour', # volume 'Gallons': 'gallon', 'Cubic Feet': 'foot ** 3', 'Cubic Meters': 'meter ** 3', 'Liters': 'liter', # volume flow 'Cubic Feet/sec': 'foot ** 3/second', 'Cubic Feet/min': 'foot ** 3/minute', 'CFm': 'foot ** 3/minute', 'CFM': 'foot ** 3/minute', 'Cubic Feet/hr': 'foot ** 3/hour', 'CFH': 'foot ** 3/hour', 'Cubic Meters/hr': 'meter ** 3 / hour', 'Gpm': 'gallon/minute', 'Gph': 'gallon/hour', 'MGD': 'megagallon/day', 'Liters/sec': 'liter/second', 'Liters/min': 'liter/minute', 'Liters/hr': 'liter/hour', # velocity 'MPH': 'mph', 'KPH': 'kph', # temperature 'Degrees C': 'degC', 'Degrees F': 'degF', 'C': 'degC', 'F': 'degF', # humidity '%RH': '', # phase 'Degrees': 'degree', # electrical 'PF': '', 'aPF': '', 'dPF': '', # intensity 'W/m^2': 'watt / meter ** 2', # dimensionless '%': '', 'PPM': '', '': '', # time 'days': 'day', 'hours': 'hour', 'minutes': 'minute', 'seconds': 'second', 'ms': 'millisecond', # pressure 'Pa': 'pascal', 'kPa': 'kilopascal', 'inWC': '', 'inAq': '', 'inHg': 'inHg', 'cmHg': 'cmHg', 'mmHg': 'mmHg', } def __init__(self): """ Initiaalizes the Obvius translator. """ self._unit_reg = pint.UnitRegistry(autoconvert_offset_to_baseunit=True) @property def parser(self) -> str: """ The `xml` parser to use for parsing the returned requester content. """ if not hasattr(self, '_parser') or \ self._parser not in self._parser_pref: for parser in self._parser_pref: if importlib.util.find_spec(parser): self._parser = parser break return self._parser @property def unit_map(self) -> dict: """ The mapping of Obvius units to valid pint units. """ if not hasattr(self, '_unit_map'): self._unit_map = {} for (k, v) in self._expression_unit_map.items(): self._unit_map[k] = self._unit_reg.parse_expression(v) return self._unit_map
[docs] def validate(self, data: str) -> bool: """ Checks if the data from the Obvius is valid. :param data: The data returned from a supported requester :type data: str :returns: True if the data is valid, otherwise False :rtype: bool """ return int( bs4.BeautifulSoup(data, self.parser) .find('error').text ) == 0
[docs] def translate(self, data: str, meta: dict={}) -> None: """ Translates Obvius data to a record. :param data: The xml returned from the Obvius endpoint :type data: str :param meta: Any additional data given to the requester :type meta: dict :returns: Does not return :rtype: None """ if self.validate(data): soup = bs4.BeautifulSoup(data, self.parser) for device_record in soup.find_all('devices'): # prepopulate the Record with meta fields that match record = Record(**meta) if not record.type or len(record.type) <= 0: const.log.warning(( 'no device type for `{record}`, ' 'default set to {device.DeviceType.UNKNOWN} ...' ).format(record=record, device=device)) record.type = device.DeviceType.UNKNOWN.name try: # discover device type device_type = device.DeviceType[record.type] record.device_name = device_record.find('name').text for rec in device_record.find_all('record'): record.timestamp = time.time() rec_error = rec.find('error').text record_data = {} for point in sorted( rec.find_all('point'), key=lambda x: int(x.attrs['number']) ): # discover valid point values and units try: rec_point_value = float(point.attrs['value']) except ValueError: rec_point_value = None try: self.unit_map[point.attrs['units']] except KeyError: point.attrs['units'] = '' # add generated RecordPoint to record_data record_data[ int(point.attrs['number']) ] = RecordPoint( name=point.attrs['name'], value=rec_point_value, unit=str(self.unit_map[ point.attrs['units'] ].units) ) # try and parse record data into reliable parsed data record.data = record_data (device_type_id, device_instance,) = device_type.value record.parsed = device_instance.parse(record) # send the generated record out to the engine self.signal.send(record) except KeyError as exc: const.log.error(( 'invalid device type `{exc.args[0]}` for ' 'record `{record}`, discarding record ...' ).format(exc=exc, record=record)) break