diff --git a/services/core/BACnetProxy/bacnet_proxy/agent.py b/services/core/BACnetProxy/bacnet_proxy/agent.py index f849a71548..8190f85e22 100644 --- a/services/core/BACnetProxy/bacnet_proxy/agent.py +++ b/services/core/BACnetProxy/bacnet_proxy/agent.py @@ -1049,7 +1049,7 @@ def read_property(self, target_address, object_type, instance_number, property_n except RuntimeError: _log.error(f"could not read {property_name} on {object_type}-{instance_number} from device {target_address}") return None - _log.debug(f"found {bacnet_results} for {property_name} on {object_type}-{instance_number} from device {target_address}") + # _log.debug(f"found {bacnet_results} for {property_name} on {object_type}-{instance_number} from device {target_address}") return bacnet_results def _get_access_spec(self, obj_data, properties): @@ -1099,28 +1099,32 @@ def find_error_object(self, read_access_list: list, target_address: str, excepti bacnet_results = {} for entry in read_access_list: - request = ReadPropertyMultipleRequest(listOfReadAccessSpecs=[entry]) - request.pduDestination = Address(target_address) - iocb = self.iocb_class(request) - self.bacnet_application.submit_request(iocb) - try: - bacnet_results.update(iocb.ioResult.get(10)) - except Exception as exc: - # _log.error(f"{exc} {target_address=} {request=}") - if "Segmentation not supported" in str(exc) or "segmentationNotSupported" in str(exc): - exc.message = f"failed to scrape: {target_address}/{entry.objectIdentifier[0]}/{entry.objectIdentifier[1]} - segmentationNotSupported" - else: - exc.message = f"failed to scrape: {target_address}/{entry.objectIdentifier[0]}/{entry.objectIdentifier[1]}" - _log.debug( - f"Point failing: {target_address}/{entry.objectIdentifier[0]}/{entry.objectIdentifier[1]} {exc=}" - ) - message = {"target_address": target_address, - "object_type": entry.objectIdentifier[0], - "instance_number": entry.objectIdentifier[1], - "exception": f"{exc}"} - self.vip.pubsub.publish(peer="pubsub", - topic="errors/bacnet", - message=message) + for prop in entry.listOfPropertyReferences: + request = ReadPropertyRequest(objectIdentifier=entry.objectIdentifier, + propertyIdentifier=prop.propertyIdentifier, + propertyArrayIndex=prop.propertyArrayIndex + ) + request.pduDestination = Address(target_address) + iocb = self.iocb_class(request) + self.bacnet_application.submit_request(iocb) + try: + bacnet_results.update(iocb.ioResult.get(10)) + except Exception as exc: + _log.error(f"{exc} {target_address=} {request=}") + if "Segmentation not supported" in str(exc) or "segmentationNotSupported" in str(exc): + exc.message = f"failed to scrape: {target_address}/{entry.objectIdentifier[0]}/{entry.objectIdentifier[1]} - segmentationNotSupported" + else: + exc.message = f"failed to scrape: {target_address}/{entry.objectIdentifier[0]}/{entry.objectIdentifier[1]}" + _log.error( + f"Point failing: {target_address}/{entry.objectIdentifier[0]}/{entry.objectIdentifier[1]} {exc=}" + ) + message = {"target_address": target_address, + "object_type": entry.objectIdentifier[0], + "instance_number": entry.objectIdentifier[1], + "exception": f"{exc}"} + self.vip.pubsub.publish(peer="pubsub", + topic="errors/bacnet", + message=message) return bacnet_results @RPC.export @@ -1171,8 +1175,10 @@ def read_properties(self, target_address, point_map, max_per_request=None, use_r bacnet_results = iocb.ioResult.get(10) except RuntimeError as exc: try: + _log.debug(f"finding error object: {exc}") bacnet_results = self.find_error_object(read_access_spec_list, target_address, exc) except PointErrorException as exc_e: + _log.debug(exc_e.message) if not hasattr(exc, "message"): _log.warning(f"exception has no message: {exc=}") # raise exc_e from exc diff --git a/services/core/PlatformDriverAgent/platform_driver/agent.py b/services/core/PlatformDriverAgent/platform_driver/agent.py index 4f1e2a38ed..88286e70db 100644 --- a/services/core/PlatformDriverAgent/platform_driver/agent.py +++ b/services/core/PlatformDriverAgent/platform_driver/agent.py @@ -58,7 +58,7 @@ utils.setup_logging() _log = logging.getLogger(__name__) -__version__ = '4.2.2' +__version__ = '4.2.3' PROMETHEUS_METRICS_FILE = "/opt/packages/prometheus_exporter/scrape_files/scrape_metrics.prom" diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/bacnet.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/bacnet.py index b031616b24..33fc10ffc7 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/bacnet.py +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/bacnet.py @@ -52,16 +52,36 @@ DEFAULT_COV_LIFETIME = 180 COV_UPDATE_BUFFER = 3 -BACNET_TYPE_MAPPING = {"multiStateValue": int, "multiStateInput": int, "multiStateOutput": int, - "analogValue": float, "analogInput": float, "analogOutput": float, - "binaryValue": bool, "binaryInput": bool, "binaryOutput": bool - } +BACNET_TYPE_MAPPING = { + "multiStateValue": int, + "multiStateInput": int, + "multiStateOutput": int, + "accumulator": int, + "analogValue": float, + "analogInput": float, + "analogOutput": float, + "binaryValue": bool, + "binaryInput": bool, + "binaryOutput": bool, +} class Register(BaseRegister): - def __init__(self, instance_number, object_type, property_name, read_only, point_name, units, - description='', priority=None, list_index=None): - super(Register, self).__init__("byte", read_only, point_name, units, description=description) + def __init__( + self, + instance_number, + object_type, + property_name, + read_only, + point_name, + units, + description="", + priority=None, + list_index=None, + ): + super(Register, self).__init__( + "byte", read_only, point_name, units, description=description + ) self.instance_number = int(instance_number) self.object_type = object_type self.property = property_name @@ -89,7 +109,9 @@ def configure(self, config_dict, registry_config_str): self.use_read_multiple = config_dict.get("use_read_multiple", True) self.timeout = float(config_dict.get("timeout", 30.0)) - self.ping_retry_interval = timedelta(seconds=config_dict.get("ping_retry_interval", 5.0)) + self.ping_retry_interval = timedelta( + seconds=config_dict.get("ping_retry_interval", 5.0) + ) self.scheduled_ping = None self.ping_target() @@ -111,16 +133,20 @@ def ping_target(self): pinged = False try: - self.vip.rpc.call(self.proxy_address, 'ping_device', self.target_address, self.device_id).get(timeout=self.timeout) + self.vip.rpc.call( + self.proxy_address, "ping_device", self.target_address, self.device_id + ).get(timeout=self.timeout) pinged = True except errors.Unreachable: _log.warning("Unable to reach BACnet proxy.") except errors.VIPError: _log.warning("Error trying to ping device.") - + except gevent.timeout.Timeout: - _log.warning(f"Timeout trying to ping device {self.target_address}. Scheduling to retry") + _log.warning( + f"Timeout trying to ping device {self.target_address}. Scheduling to retry" + ) self.scheduled_ping = None @@ -132,28 +158,44 @@ def get_point(self, point_name, get_priority_array=False): register = self.get_register_by_name(point_name) property_name = "priorityArray" if get_priority_array else register.property register_index = None if get_priority_array else register.index - result = self.vip.rpc.call(self.proxy_address, 'read_property', - self.target_address, register.object_type, - register.instance_number, property_name, register_index).get(timeout=self.timeout) + result = self.vip.rpc.call( + self.proxy_address, + "read_property", + self.target_address, + register.object_type, + register.instance_number, + property_name, + register_index, + ).get(timeout=self.timeout) return result def set_point(self, point_name, value, priority=None): # TODO: support writing from an array. register = self.get_register_by_name(point_name) if register.read_only: - raise IOError("Trying to write to a point configured read only: " + point_name) + raise IOError( + "Trying to write to a point configured read only: " + point_name + ) if priority is not None and priority < self.min_priority: - raise IOError("Trying to write with a priority lower than the minimum of " + str(self.min_priority)) + raise IOError( + "Trying to write with a priority lower than the minimum of " + + str(self.min_priority) + ) # We've already validated the register priority against the min priority. - args = [self.target_address, value, - register.object_type, - register.instance_number, - register.property, - priority if priority is not None else register.priority, - register.index] - result = self.vip.rpc.call(self.proxy_address, 'write_property', *args).get(timeout=self.timeout) + args = [ + self.target_address, + value, + register.object_type, + register.instance_number, + register.property, + priority if priority is not None else register.priority, + register.index, + ] + result = self.vip.rpc.call(self.proxy_address, "write_property", *args).get( + timeout=self.timeout + ) return result def scrape_all(self): @@ -163,17 +205,24 @@ def scrape_all(self): write_registers = self.get_registers_by_type("byte", False) for register in read_registers + write_registers: - point_map[register.point_name] = [register.object_type, - register.instance_number, - register.property, - register.index] + point_map[register.point_name] = [ + register.object_type, + register.instance_number, + register.property, + register.index, + ] while True: try: - result = self.vip.rpc.call(self.proxy_address, 'read_properties', - self.target_address, point_map, - self.max_per_request, self.use_read_multiple).get(timeout=180) - + result = self.vip.rpc.call( + self.proxy_address, + "read_properties", + self.target_address, + point_map, + self.max_per_request, + self.use_read_multiple, + ).get(timeout=180) + _log.debug(f"found {len(result)} results in platform driver") except gevent.timeout.Timeout as exc: _log.error(f"Timed out reading target {self.target_address}") @@ -181,14 +230,26 @@ def scrape_all(self): except RemoteError as exc: if "segmentationNotSupported" in exc.message: if self.max_per_request <= 1: - _log.error("Receiving a segmentationNotSupported error with 'max_per_request' setting of 1.") + _log.error( + "Receiving a segmentationNotSupported error with 'max_per_request' setting of 1." + ) raise self.register_count_divisor += 1 - self.max_per_request = max(int(self.register_count/self.register_count_divisor), 1) - _log.info("Device requires a lower max_per_request setting. Trying: "+str(self.max_per_request)) + self.max_per_request = max( + int(self.register_count / self.register_count_divisor), 1 + ) + _log.info( + "Device requires a lower max_per_request setting. Trying: " + + str(self.max_per_request) + ) continue - elif exc.message.endswith("rejected the request: 9") and self.use_read_multiple: - _log.info("Device rejected request with 'unrecognized-service' error, attempting to access with use_read_multiple false") + elif ( + exc.message.endswith("rejected the request: 9") + and self.use_read_multiple + ): + _log.info( + "Device rejected request with 'unrecognized-service' error, attempting to access with use_read_multiple false" + ) self.use_read_multiple = False continue elif self.use_read_multiple: @@ -231,19 +292,19 @@ def parse_config(self, configDict): for regDef in configDict: # Skip lines that have no address yet. - if not regDef.get('Volttron Point Name'): + if not regDef.get("Volttron Point Name"): continue - io_type = regDef.get('BACnet Object Type') - read_only = regDef.get('Writable').lower() != 'true' - point_name = regDef.get('Volttron Point Name') + io_type = regDef.get("BACnet Object Type") + read_only = regDef.get("Writable").lower() != "true" + point_name = regDef.get("Volttron Point Name") # checks if the point is flagged for change of value - is_cov = regDef.get("COV Flag", 'false').lower() == "true" + is_cov = regDef.get("COV Flag", "false").lower() == "true" - index = int(regDef.get('Index')) + index = int(regDef.get("Index")) - list_index = regDef.get('Array Index', '') + list_index = regDef.get("Array Index", "") list_index = list_index.strip() if not list_index: @@ -251,7 +312,7 @@ def parse_config(self, configDict): else: list_index = int(list_index) - priority = regDef.get('Write Priority', '') + priority = regDef.get("Write Priority", "") priority = priority.strip() if not priority: priority = None @@ -260,25 +321,34 @@ def parse_config(self, configDict): if priority < self.min_priority: message = "{point} configured with a priority {priority} which is lower than than minimum {min}." - raise DriverConfigError(message.format(point=point_name, - priority=priority, - min=self.min_priority)) - - description = regDef.get('Notes', '') - units = regDef.get('Units') - property_name = regDef.get('Property') - - register = Register(index, - io_type, - property_name, - read_only, - point_name, - units, - description=description, - priority=priority, - list_index=list_index) - - self.insert_register(register) + raise DriverConfigError( + message.format( + point=point_name, priority=priority, min=self.min_priority + ) + ) + + description = regDef.get("Notes", "") + units = regDef.get("Units") + property_name = regDef.get("Property") + + try: + register = Register( + index, + io_type, + property_name, + read_only, + point_name, + units, + description=description, + priority=priority, + list_index=list_index, + ) + + self.insert_register(register) + except Exception as exc: # pylint: disable=broad-except + _log.error( + f"Error parsing register definition: {regDef=} {exc=}" + ) if is_cov: self.cov_points.append(point_name) @@ -293,12 +363,28 @@ def establish_cov_subscription(self, point_name, lifetime, renew=False): """ register = self.get_register_by_name(point_name) try: - self.vip.rpc.call(self.proxy_address, 'create_cov_subscription', self.target_address, self.device_path, - point_name, register.object_type, register.instance_number, lifetime=lifetime) + self.vip.rpc.call( + self.proxy_address, + "create_cov_subscription", + self.target_address, + self.device_path, + point_name, + register.object_type, + register.instance_number, + lifetime=lifetime, + ) except errors.Unreachable: - _log.warning("Unable to establish a subscription via the bacnet proxy as it was unreachable.") + _log.warning( + "Unable to establish a subscription via the bacnet proxy as it was unreachable." + ) # Schedule COV resubscribe if renew and (lifetime > COV_UPDATE_BUFFER): now = datetime.now() next_sub_update = now + timedelta(seconds=(lifetime - COV_UPDATE_BUFFER)) - self.core.schedule(next_sub_update, self.establish_cov_subscription, point_name, lifetime, renew) + self.core.schedule( + next_sub_update, + self.establish_cov_subscription, + point_name, + lifetime, + renew, + ) diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/ethernetip/__init__.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/ethernetip/__init__.py new file mode 100644 index 0000000000..4998542eae --- /dev/null +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/ethernetip/__init__.py @@ -0,0 +1,221 @@ +# Copyright (c) 2020, ACE IoT Solutions LLC. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation are those +# of the authors and should not be interpreted as representing official policies, +# either expressed or implied, of the FreeBSD Project. + +""" +The Ethernet/IP Driver allows communication with PLCs and PACs that utilize the Ethernet/IP Common Industrial Protocol (CIP) +""" + +import logging +import time +import copy + +import pycomm3 + +from volttron.platform.agent import utils +from master_driver.interfaces import BaseRegister, BaseInterface, BasicRevert + +utils.setup_logging() +_log = logging.getLogger(__name__) + +class SLCRegister(BaseRegister): + """ + Generic class for containing information about a the points exposed by the TED Pro API + + + :param register_type: Type of the register. Either "bit" or "byte". Usually "byte". + :param pointName: Name of the register. + :param units: Units of the value of the register. + :param description: Description of the register. + + :type register_type: str + :type pointName: str + :type units: str + :type description: str + + """ + + def __init__(self, volttron_point_name, range_id, index, units, read_only, description): + super(SLCRegister, self).__init__("byte", + read_only, + volttron_point_name, + units, + description=description) + self.range_id = range_id + self.index = index + + +class LogixRegister(BaseRegister): + """ + Generic class for containing information about a the points exposed by the TED Pro API + + + :param register_type: Type of the register. Either "bit" or "byte". Usually "byte". + :param pointName: Name of the register. + :param units: Units of the value of the register. + :param description: Description of the register. + + :type register_type: str + :type pointName: str + :type tag_name: str + :type units: str + :type description: str + + """ + + def __init__(self, volttron_point_name, tag_name, units, read_only, description): + super(LogixRegister, self).__init__("byte", + read_only, + volttron_point_name, + units, + description=description) + self.tag_name = tag_name + + +class Interface(BasicRevert, BaseInterface): + """Create an interface for the TED device using the standard BaseInterface convention + """ + + def __init__(self, **kwargs): + super(Interface, self).__init__(**kwargs) + self.device_path = kwargs.get("device_path") + + def parse_config(self, configDict): + if configDict is None: + return + + for regDef in configDict: + # Skip lines that have no address yet. + if not regDef['Volttron Point Name']: + continue + point_path = regDef['Volttron Point Name'] + read_only = regDef.get('Writable', '').lower() == 'true' + tag_name = regDef.get('Tag Name') + range_id = regDef.get('Range ID') + index = regDef.get('Index') + description = regDef.get('Notes', '') + units = regDef.get('Units') + driver_type = regDef.get('Driver Type') + if driver_type in ('', None): + driver_type = 'Logix' + + default_value = regDef.get("Default Value", "") + if default_value is None: + default_value = "" + else: + default_value = default_value.strip() + if driver_type == 'Logix': + register = LogixRegister(point_path, tag_name, units, read_only, + description=description) + if driver_type == 'SLC': + register = SLCRegister(point_path, range_id, index, units, + read_only, description=description) + + self.insert_register(register) + + # if not read_only: + # if default_value: + # if isinstance(register, ModbusBitRegister): + # try: + # value = bool(int(default_value)) + # except ValueError: + # value = default_value.lower().startswith('t') or default_value.lower() == 'on' + # self.set_default(point_path, value) + # else: + # try: + # value = register.python_type(default_value) + # self.set_default(point_path, value) + # except ValueError: + # _log.warning("Unable to set default value for {}, bad default value in configuration. " + # "Using default revert method.".format(point_path)) + # + # else: + # _log.info("No default value supplied for point {}. Using default revert method.".format(point_path)) + + def configure(self, config_dict, registry_config_str): + """Configure method called by the master driver with configuration + stanza and registry config file, we ignore the registry config, as we + , male Dom here, How are you? + build the registers based on the configuration collected from TED Pro + Device + """ + + self.device_address = config_dict['device_address'] + self.micro800 = config_dict.get('micro800', False) + self.timeout = config_dict.get('timeout', 5) + self.init_time = time.time() + self.parse_config(registry_config_str) + + + + def _set_point(self, point_name, value): + """ + NotImplemented + """ + pass + + def get_point(self, point_name): + register = self.get_register_by_name(point_name) + if isinstance(register, LogixRegister): + with pycomm3.LogixDriver(self.device_address, micro800=self.micro800) as plc: + return plc.read(register.tag_name) + elif isinstance(register, SLCRegister): + with pycomm3.SLCDriver(self.device_address) as plc: + return plc.read(f'{register.range_id}:{register.index}') + + + def _scrape_all(self): + results = {} + read_registers = self.get_registers_by_type("byte", True) + write_registers = self.get_registers_by_type("byte", False) + + all_registers = read_registers + write_registers + slc_registers = [] + logix_registers = [] + slc_results = [] + logix_results = [] + for register in all_registers: + if isinstance(register, LogixRegister): + logix_registers.append(register) + if isinstance(register, SLCRegister): + slc_registers.append(register) + if len(slc_registers) > 0: + with pycomm3.SLCDriver(self.device_address) as plc: + slc_results = plc.read(*[f'{reg.range_id}:{reg.index}' for reg in slc_registers]) + + if len(logix_registers) > 0: + with pycomm3.LogixDriver(self.device_address, micro800=self.micro800) as plc: + logix_results = plc.read(*[reg.tag_name for reg in logix_registers]) + + for register, result in zip(slc_registers + logix_registers, + slc_results + logix_results): + try: + assert result.error is None + results[register.point_name] = result.value + except Exception as e: + _log.error("Error reading point: {}".format(repr(e))) + + return results diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/skycentrics_local/__init__.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/skycentrics_local/__init__.py new file mode 100644 index 0000000000..bc1319d47c --- /dev/null +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/skycentrics_local/__init__.py @@ -0,0 +1,125 @@ +# Copyright (c) 2019, ACE IoT Solutions LLC. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation are those +# of the authors and should not be interpreted as representing official policies, +# either expressed or implied, of the FreeBSD Project. + +""" +The Venstar Driver allows control and monitoring of Venstar Thermostats via an HTTP API +""" + +import logging +import time +import json +import mqtt + +from volttron.platform.agent import utils +from platform_driver.interfaces import BaseRegister, BaseInterface, BasicRevert +from volttron.platform.vip.agent import Agent, Core, RPC, PubSub + +_log = logging.getLogger("skycentrics_local") + +class Register(BaseRegister): + """ + Generic class for containing information about the points exposed by the Venstar API + + + :param register_type: Type of the register. Either "bit" or "byte". Usually "byte". + :param pointName: Name of the register. + :param units: Units of the value of the register. + :param description: Description of the register. + + :type register_type: str + :type pointName: str + :type units: str + :type description: str + + The TED Meter Driver does not expose the read_only parameter, as the TED API does not + support writing data. + """ + + def __init__(self, volttron_point_name, units, description): + super(Register, self).__init__("byte", + True, + volttron_point_name, + units, + description=description) + + +class Interface(BasicRevert, BaseInterface): + """Create an interface for the Venstar Thermostat using the standard BaseInterface convention + """ + + def __init__(self, **kwargs): + super(Interface, self).__init__(**kwargs) + self.device_path = kwargs.get("device_path") + self.logger = _log + self.client = mqtt.Client() + self.client.on_connect = self.on_connect + self.client.on_message = self.get_data + + def configure(self, config_dict, registry_config_str): + """Configure method called by the platform driver with configuration + stanza and registry config file + """ + pass + + def _create_registers(self, ted_config): + """ + Processes the config scraped from the device and generates + register for each available parameter + """ + return + + def _set_points(self, points): + pass + + def _set_point(self, point_name, value): + pass + + + def get_point(self, point_name): + """Get specified point""" + points = self._scrape_all() + return points.get(point_name) + + def on_connect(self): + """Run when MQTT client initially connects""" + _log.info("Connected to MQTT broker") + self.client.subscribe("#") + + def get_data(self, client, userdata, message): + """ + Continuously listen for messages from the MQTT broker + """ + payload = json.loads(message.payload) + _log.debug(f"{payload=}") + + + def _scrape_all(self): + output = {} + system_data, = self.get_data() + output = system_data + del output['name'] + return output