From dc275eae3e6027341e74f1db2a95e77faeb65505 Mon Sep 17 00:00:00 2001 From: Tommy Jonsson Date: Fri, 29 Oct 2021 15:02:53 +0200 Subject: [PATCH] 0.90.2 Add support for configuration_url, entity_category, some extra sensors (cpu, ram, net, live) --- hass_client/Client.py | 190 +++++++++----- hass_client/Devices.py | 560 ++++++++++++++++++++++++----------------- hass_client/utils.py | 15 +- setup.py | 2 +- 4 files changed, 467 insertions(+), 300 deletions(-) diff --git a/hass_client/Client.py b/hass_client/Client.py index 82b1dd8..deee328 100644 --- a/hass_client/Client.py +++ b/hass_client/Client.py @@ -1,74 +1,102 @@ # -*- coding: utf-8 -*- import json import logging -from time import gmtime, strftime - -from base import \ - Application, \ - Plugin, \ - configuration, \ - ConfigurationNumber, \ - ConfigurationString, \ - ConfigurationBool, \ - ConfigurationList, \ - implements, \ - ISignalObserver, \ - slot - -from telldus import DeviceManager +#from time import gmtime, strftime + +from base import Application, Plugin, configuration, ConfigurationNumber, ConfigurationString, ConfigurationBool, ConfigurationSelect, ConfigurationList, implements, ISignalObserver, slot # type: ignore +from hass_client.utils import getIpAddr + +from telldus import DeviceManager # type: ignore +from tellduslive.base import TelldusLive # type: ignore + import Devices as devs import logging -import paho.mqtt.client as mqtt +import paho.mqtt.client as mqtt # type: ignore __name__ = 'HASSMQTT' @configuration( - username=ConfigurationString( - defaultValue='', - title='Mqtt username', - description='Username for mqtt' - ), - password=ConfigurationString( - defaultValue='', - title='Mqtt password', - description='Password for mqtt' + device_name=ConfigurationString( + defaultValue='ztest', + title='Device name', + description='Name of this device', + sortOrder=1 ), + hostname=ConfigurationString( defaultValue='', title='Mqtt hostname', - description='Hostname for mqtt' + description='Hostname for mqtt', + sortOrder=2 ), port=ConfigurationNumber( defaultValue=1883, title='Mqtt port', - description='Port for mqtt' + description='Port for mqtt', + sortOrder=3 + ), + username=ConfigurationString( + defaultValue='', + title='Mqtt username', + description='Username for mqtt', + sortOrder=4 + ), + password=ConfigurationString( + defaultValue='', + title='Mqtt password', + description='Password for mqtt', + sortOrder=5 ), + + discovery_topic=ConfigurationString( defaultValue='hatest', title='Autodiscovery topic', - description='Homeassistants autodiscovery topic' - ), - device_name=ConfigurationString( - defaultValue='ztest', - title='Device name', - description='Name of this device' + description='Homeassistants autodiscovery topic', + sortOrder=6 ), base_topic=ConfigurationString( defaultValue='telldus', title='Base topic', - description='Base topic for this device' + description='Base topic for this device, for ex. debug', + sortOrder=7 ), + state_retain=ConfigurationBool( defaultValue=True, title='Retain state changes', - description='Post state changes with retain' + description='Post state changes with retain', + sortOrder=8 ), use_via=ConfigurationBool( defaultValue=False, - title='Use via_device', - description='Use via_device to create device hierarchy. (Does not seem to work in home-assistant yet)' + title='Create sub devices/via_device (Missing in HA)', + description='Create sub devices and setup via_device to create device hierarchy. (Does not seem to work in home-assistant yet)', + sortOrder=9 + ), + useConfigUrl=ConfigurationBool( + defaultValue=False, + title='Support device configuration url (HA >= 2021.11)', + description='Requires HA >= 2021.11.0', + sortOrder=10 + ), + configUrl=ConfigurationSelect( + defaultValue='live', + title='Configuration url', + options={ + 'live': 'Use Telldus live as url', + 'local': 'Use local IP as url' + }, + sortOrder=11 ), + useEntityCategories=ConfigurationBool( + defaultValue=False, + title='Support entity categories (HA >= 2021.11)', + description='Requires HA >= 2021.11.0', + sortOrder=12 + ), + device_topics=ConfigurationList( defaultValue=[], hidden=True @@ -83,6 +111,7 @@ class Client(Plugin): def __init__(self): Application().registerShutdown(self.onShutdown) + self.live = TelldusLive(self.context) # pylint: disable=too-many-function-args self.discovered_flag = False @@ -98,14 +127,25 @@ def __init__(self): if username != '': self.client.username_pw_set(username, password) - self.hub = devs.HaHub(self.config('device_name'), self._buildTopic) - self._debug('Hub: %s' % json.dumps(self.hub.getConfig(None, False))) + useConfigUrl = self.config('useConfigUrl') + configUrl = 'https://live.telldus.se' if self.config('configUrl') == 'live' else ('http://%s' % getIpAddr()) - self.devices = [self.hub] + self.hub = devs.HaHub(self.config('device_name'), self._buildTopic, configUrl if useConfigUrl else None) + self._debug('Hub: %s' % json.dumps(self._getDeviceConfig(self.hub))) + + self.staticDevices = [ + self.hub, + devs.HaLiveConnection(self.hub, self.live, self._buildTopic), + devs.HaIpAddr(self.hub, self._buildTopic), + devs.HaCpu(self.hub, self._buildTopic), + devs.HaRamFree(self.hub, self._buildTopic) + ] + self.devices = self.staticDevices + [] Application().queue(self.discoverAndConnect) + Application().registerScheduledTask(self._updateTimedSensors, seconds=30) def configWasUpdated(self, key, value): - if key in ['use_via', 'discovery_topic', 'device_name']: + if key in ['use_via', 'useConfigUrl', 'configUrl', 'useEntityCategories', 'discovery_topic', 'device_name']: self.devices = [] self.cleanupDevices() self.hub.deviceName = self.config('device_name') @@ -122,6 +162,10 @@ def discoverAndConnect(self): if self.config('hostname'): self.connect() + def _updateTimedSensors(self): + for haDev in [x for x in self.devices if isinstance(x, devs.HaTimedSensor)]: + self.publishState(haDev) + def _debug(self, msg): logging.info("HaClient: %s", msg) if self.mqtt_connected_flag: @@ -132,6 +176,12 @@ def _debug(self, msg): def _buildTopic(self, type, id): return '%s/%s/%s/%s' % (self.config('discovery_topic'), type, self.config('device_name'), id) + def _getDeviceConfig(self, haDev): + conf = haDev.getConfig() + if not self.config('useEntityCategories'): + conf.pop('entity_category', None) + return conf + def tearDown(self): # remove plugin self.devices = [] @@ -155,13 +205,16 @@ def connect(self): def cleanupDevices(self): if self.mqtt_connected_flag and self.discovered_flag: # clean up published devices not found - devicesConfigured = self.config('devices_configured') - if devicesConfigured and devicesConfigured != '': - oldDevs = [tuple(x) for x in json.loads(devicesConfigured)] - for type, _, fullId in oldDevs: - oldTopic = self._buildTopic(type, fullId) - self.removeDeviceTopics(oldTopic) - self.setConfig('devices_configured', None) + try: + devicesConfigured = self.config('devices_configured') + if devicesConfigured and devicesConfigured != 'None' and devicesConfigured != '': + oldDevs = [tuple(x) for x in json.loads(devicesConfigured)] + for type, _, fullId in oldDevs: + oldTopic = self._buildTopic(type, fullId) + self.removeDeviceTopics(oldTopic) + self.setConfig('devices_configured', None) + except: + pass savedTopics = self.config('device_topics') devTopics = [x.getDeviceTopic() for x in self.devices] @@ -197,11 +250,11 @@ def discover(self): self.discovered_flag = False self._debug('Discovering devices ...') - self.devices = [self.hub] + self.devices = self.staticDevices + [] devMgr = DeviceManager(self.context) for device in devMgr.retrieveDevices(): - haDevs = devs.createDevices(device, self._buildTopic) - self._debug('Discovered %s' % json.dumps(self._debugDevice(device))) + haDevs = devs.createDevices(device, self.hub, self._buildTopic, self.config('use_via')) + self._debug('Discovered %s' % json.dumps(self._debugDevice(device, haDevs))) for haDev in haDevs: self.devices.append(haDev) @@ -215,7 +268,7 @@ def publishState(self, haDev): self._debug('publish state for (%s) %s : %s' % (haDev.getID(), topic, states)) if self.mqtt_connected_flag: for state in (states if isinstance(states, list) else [states]): - self.client.publish(topic, state, 0, self.config('state_retain')) + self.client.publish(topic, str(state), 0, self.config('state_retain')) def publishDevices(self): for device in self.devices: @@ -223,7 +276,11 @@ def publishDevices(self): self.publishState(device) def publishDevice(self, haDev): - config = haDev.getConfig(self.hub, self.config('use_via')) + config = self._getDeviceConfig(haDev) + if self.config('useEntityCategories'): + config.update({ + 'entity_category': haDev.getCategory() + }) topic = '%s/config' % haDev.getDeviceTopic() self._debug('publish config for (%s) %s : %s' % (haDev.getID(), topic, json.dumps(config))) if self.mqtt_connected_flag: @@ -240,8 +297,7 @@ def removeDeviceTopics(self, devTopic): self.client.publish('%s/config' % devTopic, None, 0, True) self.client.publish('%s/state' % devTopic, None, 0, True) - def _debugDevice(self, device): - haDevs = devs.createDevices(device, self._buildTopic) + def _debugDevice(self, device, haDevs): return { 'deviceId': device.id(), 'name': device.name(), @@ -253,16 +309,16 @@ def _debugDevice(self, device): 'typeStr': device.typeString(), 'sensors': device.sensorValues(), 'state': device.state(), - 'devices': [x.getConfig(self.hub, self.config('use_via')) for x in haDevs] + 'devices': [self._getDeviceConfig(x) for x in haDevs] } @slot('deviceAdded') def onDeviceAdded(self, device): self._debug('Device added %s %s' % (device.id(), device.name())) - if device not in (x.device for x in self.devices): - haDevs = devs.createDevices(device, self._buildTopic) - self._debug('New discovery %s' % json.dumps(self._debugDevice(device))) + if device not in (x.device for x in self.devices if hasattr(x, 'device')): + haDevs = devs.createDevices(device, self.hub, self._buildTopic, self.config('use_via')) + self._debug('New discovery %s' % json.dumps(self._debugDevice(device, haDevs))) for haDev in haDevs: self.devices.append(haDev) self.publishDevice(haDev) @@ -288,7 +344,7 @@ def onDeviceUpdate(self, device): def onDeviceStateChanged(self, device, state, stateValue, origin=None): self._debug('Device state changed (%s) state: %s value: %s origin: %s' % (device.id(), state, stateValue, origin)) - haDev = next((x for x in self.devices if x.device == device), None) + haDev = next((x for x in self.devices if x.deviceId == device.id()), None) if haDev: self.publishState(haDev) else: @@ -298,9 +354,21 @@ def onDeviceStateChanged(self, device, state, stateValue, origin=None): def onSensorValueUpdated(self, device, valueType, value, scale): self._debug('Sensor value changed (%s) type: %s scale: %s value: %s' % (device.id(), valueType, scale, value)) - haDev = next((x for x in self.devices if x.device == device and x.sensorType == + haDev = next((x for x in self.devices if hasattr(x, 'device') and x.device == device and x.sensorType == valueType and x.sensorScale == scale), None) if haDev: self.publishState(haDev) else: self._debug('failed to find device for sensor change %s %s %s' % (device.id(), valueType, scale)) + + @slot('liveRegistered') + def liveRegistered(self, _msg, _refReq): + liveSensor = next(x for x in self.devices if isinstance(x, devs.HaLiveConnection)) + if liveSensor: + self.publishState(liveSensor) + + @slot('liveDisconnected') + def liveDisconnected(self): + liveSensor = next(x for x in self.devices if isinstance(x, devs.HaLiveConnection)) + if liveSensor: + self.publishState(liveSensor) diff --git a/hass_client/Devices.py b/hass_client/Devices.py index 23ba739..e43926d 100644 --- a/hass_client/Devices.py +++ b/hass_client/Devices.py @@ -1,169 +1,271 @@ -from board import Board -from utils import getMacAddr, sensorScaleIntToStr, sensorTypeIntToStr, sensorTypeIntToDeviceClass, sensorTypeIntToStateClass, slugify -from telldus import Device, Thermostat +from board import Board # type: ignore +from utils import getIpAddr, getMacAddr, sensorScaleIntToStr, sensorTypeIntToStr, sensorTypeIntToDeviceClass, sensorTypeIntToStateClass, slugify +from telldus import Device, Thermostat # type: ignore import json import logging +import psutil # type: ignore origin = 'HaClient' -def getDevice(deviceName): - return { - 'identifiers': getMacAddr(True), - 'connections': [['mac', getMacAddr(False)]], - 'manufacturer': 'Telldus Technologies', - 'model': Board.product().replace('-', ' ').title().replace(' ', '_'), - 'name': deviceName, - 'sw_version': Board.firmwareVersion() - } - - -def getViaDevice(device): - return { - 'identifiers': device.getOrCreateUUID(), - 'connections': [['mac', getMacAddr(False)]], - 'manufacturer': device.protocol().title(), - 'model': device.model().title(), - 'name': device.name(), - 'suggested_area': device.room(), - 'via_device': getMacAddr(True) - } - - -class HaHub: - def __init__(self, deviceName, buildTopic): +class HaBaseDevice(object): + def __init__(self, deviceId, deviceName, deviceType, buildTopic, viaDevice=None, category=None): + self.deviceId = deviceId self.deviceName = deviceName - self.deviceId = None - self.device = None + self.deviceType = deviceType self.buildTopic = buildTopic + self.viaDevice = viaDevice + self.category = category + + def _deviceCommand(self, device, cmd, **kwargs): + logging.info('DeviceCommand CMD: %s, ARGS: %s' % (cmd, kwargs)) + + def cmdFail(reason, **__kwargs): + logging.info('Command failed: %s' % reason) + device.command(cmd, origin=origin, failure=cmdFail, **kwargs) def getID(self): - return "hub" + return '%s' % self.deviceId + + def getName(self): + return self.deviceName def getType(self): - return 'binary_sensor' + return self.deviceType def getDeviceTopic(self): return self.buildTopic(self.getType(), self.getID()) def getState(self): - return 'online' + return None - def getConfig(self, hub, useVia): - return { - 'name': self.deviceName, + def getConfig(self): + conf = { + 'name': self.getName(), + 'unique_id': '%s_%s' % (getMacAddr(True), self.getID()), 'state_topic': '%s/state' % self.getDeviceTopic(), + } + if hasattr(self, 'runCommand'): + conf.update({'command_topic': '%s/set' % self.getDeviceTopic()}) + if self.viaDevice: + conf.update({'device': self.viaDevice}) + if self.category: + conf.update({'entity_category': self.category}) + return conf + + +class HaHub(HaBaseDevice): + def __init__(self, deviceName, buildTopic, confUrl=None): + super(HaHub, self).__init__('hub', deviceName, 'binary_sensor', buildTopic, None, 'diagnostic') + self.confUrl = confUrl + + def getState(self): + return 'online' + + def getConfig(self): + conf = super(HaHub, self).getConfig() + conf.update({ + 'device_class': 'connectivity', 'payload_on': 'online', 'payload_off': 'offline', - 'device_class': 'connectivity', - 'unique_id': getMacAddr(True), - 'device': getDevice(self.deviceName) - } + 'device': { + 'identifiers': getMacAddr(True), + #'connections': [['mac', getMacAddr(False)]], + 'manufacturer': 'Telldus Technologies', + 'model': Board.product().replace('-', ' ').title().replace(' ', '_'), + 'name': self.getName(), + 'sw_version': Board.firmwareVersion() + } + }) + if self.confUrl: + conf.update({'configuration_url': self.confUrl}) + return conf def getWillState(self): - return "offline" + return 'offline' + + +class HaHubDevice(HaBaseDevice): + def __init__(self, hub, deviceId, deviceName, deviceType, buildTopic, viaDevice=None, category=None): + super(HaHubDevice, self).__init__( + deviceId, + deviceName, + deviceType, + buildTopic, + viaDevice or {'identifiers': hub.getConfig().get('device', {}).get('identifiers', '')}, + category + ) + self.hub = hub + def getConfig(self): + conf = super(HaHubDevice, self).getConfig() + conf.update({ + 'availability_topic': '%s/state' % self.hub.getDeviceTopic() + }) + return conf -class HaDevice: - def __init__(self, device, buildTopic): - self.deviceId = device.id() - self.device = device - self.buildTopic = buildTopic - def getID(self): - return '%s' % self.device.id() +class HaHubSensor(HaHubDevice): + def __init__(self, hub, deviceId, deviceName, buildTopic, viaDevice=None, category=None, unit=None): + super(HaHubSensor, self).__init__(hub, deviceId, deviceName, 'sensor', buildTopic, viaDevice=viaDevice, category=category) + self.unit = unit - def getType(self): - return 'binary_sensor' + def getConfig(self): + conf = super(HaHubSensor, self).getConfig() + conf.update({'unit_of_measurement': self.unit or ''}) + return conf + + +class HaHubConnectivitySensor(HaHubDevice): + def __init__(self, hub, deviceId, deviceName, buildTopic, viaDevice=None, category=None): + super(HaHubConnectivitySensor, self).__init__(hub, deviceId, deviceName, + 'binary_sensor', buildTopic, viaDevice=viaDevice, category=category) + + def getConfig(self): + conf = super(HaHubConnectivitySensor, self).getConfig() + conf.update({ + 'device_class': 'connectivity', + 'payload_on': 'online', + 'payload_off': 'offline' + }) + return conf - def getDeviceTopic(self): - return self.buildTopic(self.getType(), self.getID()) + +class HaTimedSensor(HaBaseDevice): + pass + + +class HaLiveConnection(HaHubConnectivitySensor, HaTimedSensor): + def __init__(self, hub, live, buildTopic): + super(HaLiveConnection, self).__init__(hub, 'live', 'Telldus live', buildTopic, None, 'diagnostic') + self.live = live def getState(self): - state, stateValue = self.device.state() - logging.info('Device (%s) state : %s %s' % (self.getID(), state, stateValue)) - return 'ON' if state == Device.TURNON else 'OFF' + return 'online' if self.live.registered else 'offline' - def getConfig(self, hub, useVia): - conf = { - 'name': self.device.name(), - 'unique_id': '%s_%s' % (getMacAddr(True), self.getID()), - 'state_topic': '%s/state' % self.getDeviceTopic(), - 'availability_topic': '%s/state' % hub.getDeviceTopic(), - 'device': getViaDevice(self.device) if useVia else {'identifiers': getMacAddr(True)} - } - if hasattr(self, 'runCommand'): - conf.update({'command_topic': '%s/set' % self.getDeviceTopic()}) +class HaIpAddr(HaHubSensor, HaTimedSensor): + def __init__(self, hub, buildTopic): + super(HaIpAddr, self).__init__(hub, 'ipaddr', 'IP address', buildTopic, None, 'diagnostic', None) - return conf + def getState(self): + return getIpAddr() - def _runCommand(self, topic, command): - logging.info('runCommand %s %s %s %s', self.getType(), self.getID(), topic, command) - def _deviceCommand(self, cmd, **kwargs): - logging.info('DeviceCommand CMD: %s, ARGS: %s' % (cmd, kwargs)) +class HaCpu(HaHubSensor, HaTimedSensor): + def __init__(self, hub, buildTopic): + super(HaCpu, self).__init__(hub, 'cpu', 'Cpu usage', buildTopic, None, 'diagnostic', '%') - def cmdFail(reason, **__kwargs): - logging.info('Command failed: %s' % reason) - self.device.command(cmd, origin=origin, failure=cmdFail, **kwargs) + def getState(self): + return psutil.cpu_percent(1) -class HaRemote(HaDevice): - def getType(self): - return 'binary_sensor' +class HaRamFree(HaHubSensor, HaTimedSensor): + def __init__(self, hub, buildTopic): + super(HaRamFree, self).__init__(hub, 'ram_free', 'Free ram', buildTopic, None, None, '%') - def getConfig(self, hub, useVia): - sConf = HaDevice.getConfig(self, hub, useVia).copy() - sConf.update({'expire_after': 1}) - return sConf + def getState(self): + return int(psutil.virtual_memory().available * 100 / psutil.virtual_memory().total) -class HaCover(HaDevice): - def getType(self): - return 'cover' +class HaNetIOSent(HaHubSensor, HaTimedSensor): + def __init__(self, hub, buildTopic): + super(HaNetIOSent, self).__init__(hub, 'net_sent', 'Network sent bytes', buildTopic, None, None, 'Mb') def getState(self): - state, stateValue = self.device.state() - logging.info('Cover (%s) state : %s %s' % (self.getID(), state, stateValue)) - if self.device.methods() & Device.DIM: - return int(stateValue) - else: - return 'open' if state == Device.UP else \ - 'closed' if state == Device.DOWN else \ - 'stopped' + return int(psutil.net_io_counters().bytes_sent / 1024) - def getConfig(self, hub, useVia): - sConf = HaDevice.getConfig(self, hub, useVia).copy() - if self.device.methods() & Device.DIM: - sConf.update({ - 'position_topic': '%s/state' % self.getDeviceTopic(), - 'set_position_topic': '%s/pos' % self.getDeviceTopic(), - 'position_open': 0, - 'position_closed': 255 + +class HaNetIORecv(HaHubSensor, HaTimedSensor): + def __init__(self, hub, buildTopic): + super(HaNetIORecv, self).__init__(hub, 'net_recv', 'Network recv bytes', buildTopic, None, None, 'Mb') + + def getState(self): + return psutil.net_io_counters().bytes_recv / 1024 + + +class HaDeviceSensor(HaHubSensor): + def __init__(self, hub, device, sensorType, sensorScale, buildTopic, viaDevice=None, category=None, unit=None): + super(HaDeviceSensor, self).__init__( + hub, + '%s_%s_%s' % (device.id(), sensorType, sensorScale), + '%s %s' % (device.name(), sensorTypeIntToStr(sensorType, sensorScale)), + buildTopic, + viaDevice=viaDevice, + category=category, + unit=unit or sensorScaleIntToStr(sensorType, sensorScale) + ) + self.device = device + self.sensorType = sensorType + self.sensorScale = sensorScale + + def getState(self): + sensor = next((x for x in self.device.sensorValues()[self.sensorType] if x['scale'] == self.sensorScale), None) + if sensor: + return json.dumps({ + 'value': sensor.get('value', None), + 'lastUpdated': sensor.get('lastUpdated', None) }) - return sConf - def runCommand(self, topic, command): - HaDevice._runCommand(self, topic, command) - topicType = topic.split('/')[-1].upper() - if topicType == 'POS': - self._deviceCommand(Device.DIM, int(command)) - elif topicType == 'SET': - self._deviceCommand( - Device.UP if command.upper() == 'OPEN' - else Device.DOWN if command.upper() == 'CLOSE' else - Device.STOP - ) + def getConfig(self): + conf = super(HaDeviceSensor, self).getConfig() + conf.update({ + 'state_class': sensorTypeIntToStateClass(self.sensorType, self.sensorScale), + 'value_template': '{{ value_json.value }}' + }) + devClass = sensorTypeIntToDeviceClass(self.sensorType, self.sensorScale) + if devClass: + conf.update({'device_class': devClass}) + return conf -class HaLight(HaDevice): - def getType(self): - return 'light' +class HaDeviceBinary(HaHubDevice): + def __init__(self, hub, device, buildTopic, viaDevice=None, category=None): + super(HaDeviceBinary, self).__init__(hub, device.id(), device.name(), + 'binary_sensor', buildTopic, viaDevice=viaDevice, category=category) + self.device = device + + def getState(self): + state, stateValue = self.device.state() + return 'ON' if state == Device.TURNON else 'OFF' + + +class HaDeviceSwitch(HaHubDevice): + def __init__(self, hub, device, buildTopic, viaDevice=None, category=None): + super(HaDeviceSwitch, self).__init__(hub, device.id(), device.name(), + 'switch', buildTopic, viaDevice=viaDevice, category=category) + self.device = device + + def getState(self): + state, stateValue = self.device.state() + result = 'ON' if state in [Device.TURNON, Device.BELL] else 'OFF' + if state == Device.BELL: + return [result, 'OFF'] + return result + + def getConfig(self): + conf = super(HaDeviceSwitch, self).getConfig() + if self.device.methods() & Device.BELL: + conf.update({ 'payload_on': 'BELL' }) + return conf + + def runCommand(self, topic, payload): + self._deviceCommand( + self.device, + Device.TURNON if payload.upper() == 'ON' \ + else Device.BELL if payload.upper() == 'BELL' \ + else Device.TURNOFF + ) + + +class HaDeviceLight(HaHubDevice): + def __init__(self, hub, device, buildTopic, viaDevice=None, category=None): + super(HaDeviceLight, self).__init__(hub, device.id(), device.name(), + 'light', buildTopic, viaDevice=viaDevice, category=category) + self.device = device def getState(self): state, stateValue = self.device.state() - logging.info('Light (%s) state : %s %s' % (self.getID(), state, stateValue)) if state == Device.DIM: return json.dumps({ 'state': 'ON' if stateValue and int(stateValue) > 0 else 'OFF', @@ -175,61 +277,84 @@ def getState(self): 'brightness': (int(stateValue) if stateValue else 255) if state == Device.TURNON else 0 }) - def getConfig(self, hub, useVia): - sConf = HaDevice.getConfig(self, hub, useVia).copy() - sConf.update({'schema': 'json', 'brightness': True}) - return sConf - - def runCommand(self, topic, command): - HaDevice._runCommand(self, topic, command) - payload = json.loads(command) - if 'brightness' in payload: - if int(payload['brightness']) == 0: - self._deviceCommand(Device.TURNOFF) + def getConfig(self): + conf = super(HaDeviceLight, self).getConfig() + conf.update({ + 'schema': 'json', + 'brightness': True + }) + return conf + + def runCommand(self, topic, payload): + command = json.loads(payload) + if 'brightness' in command: + if int(command['brightness']) == 0: + self._deviceCommand(self.device, Device.TURNOFF) else: - self._deviceCommand(Device.DIM, value=int(payload['brightness'])) + self._deviceCommand(self.device, Device.DIM, value=int(command['brightness'])) else: self._deviceCommand( - Device.TURNON if payload['state'].upper() == 'ON' + self.device, + Device.TURNON if command['state'].upper() == 'ON' else Device.TURNOFF, value=255 ) -class HaSwitch(HaDevice): - def getType(self): - return 'switch' +class HaDeviceRemote(HaDeviceBinary): + def __init__(self, hub, device, buildTopic, viaDevice=None, category=None): + super(HaDeviceRemote, self).__init__(hub, device, buildTopic, viaDevice, category) + + def getConfig(self): + conf = super(HaDeviceRemote, self).getConfig() + conf.update({'expire_after': 1}) + return conf + + +class HaDeviceCover(HaHubDevice): + def __init__(self, hub, device, buildTopic, viaDevice=None, category=None): + super(HaDeviceCover, self).__init__(hub, device.id(), device.name(), + 'cover', buildTopic, viaDevice=viaDevice, category=category) + self.device = device def getState(self): state, stateValue = self.device.state() - logging.info('Switch (%s) state : %s %s' % (self.getID(), state, stateValue)) - result = 'ON' if state in [Device.TURNON, Device.BELL] else 'OFF' - if state == Device.BELL: - return [result, 'OFF'] - return result - - def getConfig(self, hub, useVia): - sConf = HaDevice.getConfig(self, hub, useVia).copy() - if self.device.methods() & Device.BELL: - sConf.update({'payload_on': 'BELL'}) - return sConf + if self.device.methods() & Device.DIM: + return int(stateValue) + else: + return 'open' if state == Device.UP else \ + 'closed' if state == Device.DOWN else \ + 'stopped' - def runCommand(self, topic, command): - HaDevice._runCommand(self, topic, command) - self._deviceCommand( - Device.TURNON if command.upper() == 'ON' - else Device.BELL if command.upper() == 'BELL' - else Device.TURNOFF - ) + def getConfig(self): + conf = super(HaDeviceCover, self).getConfig() + if self.device.methods() & Device.DIM: + conf.update({ + 'position_topic': '%s/state' % self.getDeviceTopic(), + 'set_position_topic': '%s/pos' % self.getDeviceTopic(), + 'position_open': 0, + 'position_closed': 255 + }) + return conf + def runCommand(self, topic, payload): + topicType = topic.split('/')[-1].upper() + if topicType == 'POS': + self._deviceCommand(self.device, Device.DIM, value=int(payload)) + elif topicType == 'SET': + self._deviceCommand( + self.device, + Device.UP if payload.upper() == 'OPEN' + else Device.DOWN if payload.upper() == 'CLOSE' else + Device.STOP + ) -class HaClimate(HaDevice): - def __init__(self, device, buildTopic, sensors): - HaDevice.__init__(self, device, buildTopic) - self.sensors = sensors - def getType(self): - return 'hvac' +class HaDeviceClimate(HaHubDevice): + def __init__(self, hub, device, buildTopic, viaDevice=None, category=None): + super(HaDeviceClimate, self).__init__(hub, device.id(), device.name(), + 'hvac', buildTopic, viaDevice=viaDevice, category=category) + self.device = device def _getThermostat(self): params = self.device.allParameters() if hasattr(self.device, 'allParameters') else self.device.parameters() @@ -242,8 +367,8 @@ def _getSetPoints(self): return self._getThermostat().get('setpoints', {}) def getState(self): - temp = self.device.sensorValue(Device.TEMPERATURE, Device.SCALE_TEMPERATURE_CELCIUS) or self.device.sensorValue( - Device.TEMPERATURE, Device.SCALE_TEMPERATURE_FAHRENHEIT) + temp = self.device.sensorValue(Device.TEMPERATURE, Device.SCALE_TEMPERATURE_CELCIUS) or \ + self.device.sensorValue(Device.TEMPERATURE, Device.SCALE_TEMPERATURE_FAHRENHEIT) thermoValues = self.device.stateValue(Device.THERMOSTAT, {}) mode = thermoValues.get('mode', None) setPoint = thermoValues.get('setpoint', {}) @@ -255,12 +380,12 @@ def getState(self): 'mode': mode }) - def getConfig(self, hub, useVia): - sConf = HaDevice.getConfig(self, hub, useVia) + def getConfig(self): + conf = super(HaDeviceClimate, self).getConfig() modes = self._getModes() if len(modes) > 0: - sConf.update({ + conf.update({ 'modes': modes, 'mode_state_topic': '%s/state' % self.getDeviceTopic(), 'mode_state_template': '{{ value_json.mode }}', @@ -269,7 +394,7 @@ def getConfig(self, hub, useVia): setPoints = self._getSetPoints() if len(setPoints) > 0: - sConf.update({ + conf.update({ 'temperature_state_topic': '%s/state' % self.getDeviceTopic(), 'temperature_state_template': '{{ value_json.setpoint }}', 'temperature_command_topic': '%s/setPoint' % self.getDeviceTopic() @@ -279,122 +404,87 @@ def getConfig(self, hub, useVia): sensorValues = self.device.sensorValues().get(Device.TEMPERATURE, []) tempValue = next(sensorValues, None) if tempValue: - sConf.update({ + conf.update({ 'current_temperature_topic': '%s/state' % self.getDeviceTopic(), 'current_temperature_template': '{{ value_json.temperature }}', - 'unit_of_measurement': sensorScaleIntToStr(Device.TEMPERATURE, tempValue.scale) + 'unit_of_measurement': sensorScaleIntToStr(Device.TEMPERATURE, tempValue.scale) or '' }) - return sConf + return conf - def runCommand(self, topic, command): - HaDevice._runCommand(self, topic, command) + def runCommand(self, topic, payload): topicType = topic.split('/')[-1].upper() if topicType == 'SETMODE': value = { - 'mode': command, + 'mode': payload, 'changeMode': True, } - self._deviceCommand(Device.THERMOSTAT, value=value) + self._deviceCommand(self.device, Device.THERMOSTAT, value=value) elif topicType == 'SETPOINT': - setpoint = float(command) if command else None + setpoint = float(payload) if payload else None value = { 'changeMode': False, 'temperature': setpoint } - self._deviceCommand(Device.THERMOSTAT, value=value) - + self._deviceCommand(self.device, Device.THERMOSTAT, value=value) -class HaBattery(HaDevice): - def getID(self): - sID = HaDevice.getID(self) - return '%s_battery' % sID - def getType(self): - return 'sensor' +class HaDeviceBattery(HaHubSensor): + def __init__(self, hub, device, buildTopic, viaDevice=None, category=None, unit=None): + super(HaDeviceBattery, self).__init__(hub, '%s_battery' % device.id(), + device.name(), buildTopic, viaDevice=viaDevice, category=category, unit=unit) + self.device = device def getState(self): level = self.device.battery() - return { - Device.BATTERY_LOW: 1, - Device.BATTERY_UNKNOWN: None, - Device.BATTERY_OK: 100 - }.get(level, int(level)) - - def getConfig(self, hub, useVia): - sConf = HaDevice.getConfig(self, hub, useVia).copy() - sConf.update({ - 'name': '%s battery' % sConf.get('name'), + return {Device.BATTERY_LOW: 1, Device.BATTERY_OK: 100, Device.BATTERY_UNKNOWN: None}.get(level, int(level)) + + def getConfig(self): + conf = super(HaDeviceBattery, self).getConfig() + conf.update({ + 'name': '%s battery' % conf.get('name'), 'device_class': 'battery', 'state_class': 'measurement' }) - return sConf - - -class HaSensor(HaDevice): - def __init__(self, device, buildTopic, sensorType, sensorScale): - HaDevice.__init__(self, device, buildTopic) - self.sensorType = sensorType - self.sensorScale = sensorScale - - def getID(self): - sID = HaDevice.getID(self) - return '%s_%s_%s' % (sID, self.sensorType, self.sensorScale) - - def getType(self): - return 'sensor' - - def getState(self): - sensor = next((x for x in self.device.sensorValues()[ - self.sensorType] if x['scale'] == self.sensorScale), None) - if sensor: - return json.dumps({ - 'value': sensor.get('value', None), - 'lastUpdated': sensor.get('lastUpdated', None) - }) - - return None - - def getConfig(self, hub, useVia): - sConf = HaDevice.getConfig(self, hub, useVia).copy() - sConf.update({ - 'name': '%s %s' % (sConf.get('name'), sensorTypeIntToStr(self.sensorType, self.sensorScale)), - 'state_class': sensorTypeIntToStateClass(self.sensorType, self.sensorScale), - 'value_template': '{{ value_json.value }}', - 'unit_of_measurement': sensorScaleIntToStr(self.sensorType, self.sensorScale) - }) - devClass = sensorTypeIntToDeviceClass(self.sensorType, self.sensorScale) - if devClass: - sConf.update({'device_class': devClass}) - return sConf + return conf -def createDevices(device, buildTopic): +def createDevices(device, hub, buildTopic, createSubDevices=False): caps = device.methods() devType = device.allParameters().get('devicetype') result = [] + subDevice = { + 'identifiers': device.getOrCreateUUID(), + #'connections': [['mac', getMacAddr(False)]], + 'manufacturer': device.protocol().title(), + 'model': device.model().title(), + 'name': device.name(), + 'suggested_area': device.room() or '', + 'via_device': hub.getConfig().get('device', {}).get('identifiers', '') + } if createSubDevices else None + if device.battery() and device.battery() != Device.BATTERY_UNKNOWN: - result.append(HaBattery(device, buildTopic)) + result.append(HaDeviceBattery(hub, device, buildTopic, subDevice)) if device.isSensor(): for type, sensors in device.sensorValues().items(): for sensor in sensors: - result.append(HaSensor(device, buildTopic, type, sensor['scale'])) + result.append(HaDeviceSensor(hub, device, type, sensor.get('scale', 0), buildTopic, subDevice)) if device.isDevice(): if devType == Device.TYPE_THERMOSTAT: - result.append(HaClimate(device, buildTopic, [x for x in result if isinstance(x, HaSensor)])) + result.append(HaDeviceClimate(hub, device, buildTopic, subDevice)) elif devType == Device.TYPE_REMOTE_CONTROL: - result.append(HaRemote(device, buildTopic)) + result.append(HaDeviceRemote(hub, device, buildTopic, subDevice)) elif devType == Device.TYPE_WINDOW_COVERING or caps & Device.UP and caps & Device.DOWN: - result.append(HaCover(device, buildTopic)) + result.append(HaDeviceCover(hub, device, buildTopic, subDevice)) elif devType == Device.TYPE_LIGHT or caps & Device.DIM: - result.append(HaLight(device, buildTopic)) + result.append(HaDeviceLight(hub, device, buildTopic, subDevice)) elif caps & Device.BELL or caps & Device.TURNON: - result.append(HaSwitch(device, buildTopic)) + result.append(HaDeviceSwitch(hub, device, buildTopic, subDevice)) else: - result.append(HaDevice(device, buildTopic)) + result.append(HaDeviceBinary(hub, device, buildTopic, subDevice)) return result diff --git a/hass_client/utils.py b/hass_client/utils.py index db29c3f..371191a 100644 --- a/hass_client/utils.py +++ b/hass_client/utils.py @@ -1,7 +1,7 @@ from time import gmtime, strftime -import netifaces -from board import Board -from telldus import Device +import netifaces # type: ignore +from board import Board # type: ignore +from telldus import Device # type: ignore def getMacAddr(compact=True): @@ -13,6 +13,15 @@ def getMacAddr(compact=True): return mac.upper().replace(':', '') if compact else mac.upper() +def getIpAddr(): + iface = netifaces.ifaddresses(Board.networkInterface()) + try: + inet = iface[netifaces.AF_INET][0] + except: + inet = {'addr': ''} + return inet.get('addr', '') + + def slugify(value): allowed_chars = set('_0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') return filter(lambda x: x in allowed_chars, value.replace(' ', '_').replace('-', '_')) diff --git a/setup.py b/setup.py index db1f724..c690de6 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name='MQTT Homeassistant', - version='0.90.1', + version='0.90.2', description='Plugin to connect to Homeassistant via MQTT Autodiscover', icon='hass.png', color='#660066',