├── .gitignore ├── README.md ├── custom_components ├── .gitignore ├── device │ ├── 3010.py │ ├── __init__.py │ ├── default.py │ ├── lumi_plug.py │ ├── lumi_remote_b1acn01.py │ ├── lumi_remote_b286acn01.py │ ├── lumi_sensor_86sw2.py │ ├── lumi_sensor_86sw2un.py │ ├── lumi_sensor_cube.py │ ├── lumi_sensor_cube_aqgl01.py │ ├── lumi_sensor_ht.py │ ├── lumi_sensor_magnet.py │ ├── lumi_sensor_magnet_aq2.py │ ├── lumi_sensor_motion.py │ ├── lumi_sensor_motion_aq2.py │ ├── lumi_sensor_switch.py │ ├── lumi_sensor_switch_aq2.py │ ├── lumi_sensor_switch_aq3.py │ ├── lumi_sensor_wleak_aq1.py │ ├── lumi_vibration_aq1.py │ ├── lumi_weather.py │ ├── multi.py │ ├── plug_01.py │ ├── sp31.py │ ├── tradfri_remote_control.py │ └── tradfri_wireless_dimmer.py └── zha_new │ ├── __init__.py │ ├── binary_sensor.py │ ├── cluster_handler.py │ ├── const.py │ ├── helpers.py │ ├── light.py │ ├── manifest.json │ ├── sensor.py │ ├── services.yaml │ └── switch.py └── zigbee2dot.py /.gitignore: -------------------------------------------------------------------------------- 1 | .eric6project/ 2 | _eric6project/ 3 | .eric5project/ 4 | _eric5project/ 5 | .eric4project/ 6 | _eric4project/ 7 | .ropeproject/ 8 | _ropeproject/ 9 | .directory/ 10 | *.pyc 11 | *.pyo 12 | *.orig 13 | *.bak 14 | *.rej 15 | *~ 16 | cur/ 17 | tmp/ 18 | __pycache__/ 19 | *.DS_Store 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Never ever open the zha integration under configuration, this overwrites my bellows/zigpy 3 | 4 | # Incompatible with mainstream zigpy/bellows 5 | quirks code in mainstream zigpy breaks compatiblilty with zha_new, please use my versions 6 | With the patch from estevez-dev my zigpy and bellows should be loaded automatic 7 | 8 | # 01/26/2019 9 | - initial lightlink support to read out group id from remotes and controllers 10 | - bugfixes 11 | - additional device types for lights 12 | # 01/13/2019 13 | new master release, please update to hass >84.1 14 | - restore states from history after restart 15 | - more devices 16 | - improved stability 17 | - rewrote light handler, read max/min mireds from bulb if posible, get state reports from bulbs, powered-on bulbs get detected asap. 18 | -much more 19 | - new controller frame statistics 20 | 21 | # ha-zha-new 22 | see wiki for tips 23 | ## Breaking update 24 | switched from old bellows to zigpy/zigpy and zigpy/bellows 25 | Most of my needed changes are in the original zigpy distro. I will create PR´s for the others. For newer features or to be save use my bellows&zigpy fork. 26 | ### update of the zha component 27 | based on the work from rcloran and others 28 | Converted to a custom_component for an easier way to test and distribute to others without changing the homeassistant code. Should be forked back to the HA codeafter testing. 29 | 30 | ## NEW REMOTES 31 | - tested for tradfri dimmer and remotes 32 | - created a virtual device with On/Off, Level, Scenes(also a kind of Level for colortemp) 33 | - sends events upstream to HASS for scripting 34 | - event data 35 | - 'entity_id': self._entity.entity_id 36 | - 'channel': OnOff, Level or Scenes, it is the cluster name where a command comes in 37 | - 'command': the incoming comand name, eg : on,off,toggle,move, move_with_on_of... 38 | - 'up_down': 1 or -1 for increase descrease 39 | - 'step': how much up or down 40 | ## NEW 41 | - metering/on-off support for sitecom WLE-1000 plugs with reporting 42 | - Aquara Water sensors as binary-sensors 43 | - add RSSI/LQI information in entity attributes 44 | - added zha_new object to monitor some parameters, which is helpful to see whats going on during pairing 45 | - device left 46 | - permit enabled 47 | - device joined 48 | - device init - settting up device in hass 49 | - run - normal operations 50 | 51 | ## tested devices 52 | - xiaomi and aquara sensors 53 | - tradfri bulbs 54 | - tradfri dimmer 55 | 56 | ## loadable device handler 57 | create your own device handlers for unsupported devices, see files under custom_components/devices 58 | 59 | ## use master-pre-0,61 branch for HA <= 0.60 and master- >= 0.63 60 | pre 0.61 code - no updates 61 | 62 | ## Todo 63 | - detect returning endpoints and update state ( mainly for bulbs), needs endpoint 0 (zdo) enabled in zha Ha component 64 | 65 | 66 | 67 | ##USAGE: 68 | check out inside your $home/.homeassistant/ directory, code needs to be in custom_component 69 | 70 | **configuration example:** 71 | tradfri bulbs and dimmer need to use the template definition, xiaome sensors gets autodetected 72 | 73 | 74 | #my current zha config with xiaomi sensors and tradfri bulbs 75 | zha_new: 76 | usb_path: /dev/ttyUSB0 77 | database_path: /home/homeassistant/.homeassistant/zigbee.db 78 | device_config: 79 | # tradfri dimmer 80 | "00:0b:57:ff:fe:24:18:9f-1": 81 | template: tradfri_dimmer 82 | # tradfri dimmable bulbs 83 | "00:0b:57:ff:fe:2d:ab:35-1": 84 | template: TRADFRI_bulb 85 | "00:0b:57:ff:fe:b2:d3:b7-1": 86 | template: TRADFRI_bulb 87 | 88 | ## History 89 | ## 1/8 dev-loader branch merged into master ->added support for: 90 | - tradfri dimmable bulbs, not tested for the temperature bulbs, but maybe working 91 | - loadable device handler modules in custom_components/device/ 92 | - to parse attribute reports 93 | - to initalize endpoint based on model name 94 | - template support, templates in device directoy 95 | - auto detect xiaomi sensors, tested with the xiaomi original sensors, aqara work, but some attributes are not correct ->TODO 96 | - added pressure sensor, works for Aqara weather sensor -> TODO: need to separate original xiaomi and aquare templates 97 | 98 | ### Master branch 99 | - device specific modules, get loaded based on model 100 | - xiaomi battery and other attributes 101 | - working Xiaomi Door/windows sensor as binary_sensor with state updates inside HA 102 | - working Xiaomi HT sensors inside HA 103 | - use in_cluster and out_cluster to override predefined cluster_profile from bellows, that not match non_standard devices 104 | - configure reporting inside configuration.yaml 105 | - working original xiaomi motion sensor, clear detection after 20 sec, as sensor only send detection reports, but no "clear" reports 106 | - if a device leaves, the device gets removed from the database. Thus, you can unpair and pair a device now, without need to clear the database 107 | - see the zha.yaml file for configuration 108 | - it will create a base entity and a entity for each sensor(motion, temp, humidity, on/off) 109 | 110 | -------------------------------------------------------------------------------- /custom_components/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .gitignore 6 | .eric6project 7 | *.e4p 8 | -------------------------------------------------------------------------------- /custom_components/device/3010.py: -------------------------------------------------------------------------------- 1 | """" custom py file for device NYCE 3014.""" 2 | import logging 3 | _LOGGER = logging.getLogger(__name__) 4 | 5 | 6 | def _custom_endpoint_init(self, node_config, *argv): 7 | """set node_config based obn Lumi device_type.""" 8 | config = {} 9 | selector = node_config.get('template', None) 10 | if not selector: 11 | selector = argv[0] 12 | _LOGGER.debug(" selector: %s", selector) 13 | config = { 14 | "config_report": [ 15 | [0x0001, 0x0020, 60, 3600, 5], 16 | [0x0001, 0x0021, 60, 3600, 5] 17 | ], 18 | "in_cluster": [0x0000, 0x0001, 0x0500, ], 19 | "out_cluster": [0x0500], 20 | "type": "binary_sensor", 21 | } 22 | self.add_input_cluster(0x0500) 23 | self.add_output_cluster(0x0500) 24 | node_config.update(config) 25 | -------------------------------------------------------------------------------- /custom_components/device/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoda-x/ha-zha-new/9f6058ff25daf2df71d912d2e2e6b3876ffb7f4d/custom_components/device/__init__.py -------------------------------------------------------------------------------- /custom_components/device/default.py: -------------------------------------------------------------------------------- 1 | """ default template. """ 2 | 3 | 4 | def _custom_endpoint_init(self, node_config, *argv): 5 | """ dummy function.""" 6 | pass 7 | 8 | 9 | def _parse_attribute(entity, attrib, value, *argv): 10 | """ dummy function.""" 11 | return(attrib, value) 12 | 13 | 14 | def _custom_cluster_command(self, tsn, command_id, args): 15 | """ dummy function.""" 16 | pass 17 | -------------------------------------------------------------------------------- /custom_components/device/lumi_plug.py: -------------------------------------------------------------------------------- 1 | """" custom py file for device.""" 2 | import logging 3 | import asyncio 4 | import homeassistant.util.dt as dt_util 5 | from custom_components import zha_new 6 | 7 | _LOGGER = logging.getLogger(__name__) 8 | 9 | 10 | def _custom_endpoint_init(self, node_config, *argv): 11 | """set node_config based on Lumi device_type.""" 12 | config = {} 13 | selector = node_config.get('template', None) 14 | if not selector: 15 | selector = argv[0] 16 | if self.endpoint_id == 1: 17 | config = { 18 | "in_cluster": [0x0000, 0x0006], 19 | "type": "switch", 20 | } 21 | node_config.update(config) 22 | elif self._endpoint_id == 2: 23 | config = { 24 | "config_report": [ 25 | [0x000c, 0x0055, 0, 1800, 5], 26 | ], 27 | "in_cluster": [0x0000, 0x000c], 28 | "out_cluster": [], 29 | "type": "sensor", 30 | } 31 | self.add_input_cluster(0x000c) 32 | node_config.update(config) 33 | 34 | 35 | def _parse_attribute(entity, attrib, value, *argv, **kwargs): 36 | """parse non standard attributes.""" 37 | import zigpy.types as t 38 | from zigpy.zcl import foundation as f 39 | if type(value) is str: 40 | result = bytearray() 41 | result.extend(map(ord, value)) 42 | value = result 43 | if entity.entity_connect == {}: 44 | entity_store = zha_new.get_entity_store(entity.hass) 45 | device_store = entity_store.get(entity._endpoint._device._ieee, {}) 46 | for dev_ent in device_store: 47 | if hasattr(dev_ent, 'cluster_key'): 48 | entity.entity_connect[dev_ent.cluster_key] = dev_ent 49 | attributes = {} 50 | if attrib == 85: 51 | result = float(t.Double(value)) 52 | attributes["power"] = float(t.Double(value)) 53 | attributes["unit_of_measurement"] = 'W' 54 | attrib = 0 55 | else: 56 | result = value 57 | attributes["Last seen"] = dt_util.now() 58 | if "path" in attributes: 59 | entity._endpoint._device.handle_RouteRecord(attributes["path"]) 60 | entity._device_state_attributes.update(attributes) 61 | return(attrib, result) 62 | -------------------------------------------------------------------------------- /custom_components/device/lumi_remote_b1acn01.py: -------------------------------------------------------------------------------- 1 | from .lumi_sensor_86sw2 import _custom_endpoint_init, _parse_attribute 2 | -------------------------------------------------------------------------------- /custom_components/device/lumi_remote_b286acn01.py: -------------------------------------------------------------------------------- 1 | from .lumi_sensor_86sw2 import _custom_endpoint_init, _parse_attribute -------------------------------------------------------------------------------- /custom_components/device/lumi_sensor_86sw2.py: -------------------------------------------------------------------------------- 1 | """" custom py file for device.""" 2 | import logging 3 | import homeassistant.util.dt as dt_util 4 | 5 | _LOGGER = logging.getLogger(__name__) 6 | 7 | 8 | def _custom_endpoint_init(self, node_config, *argv): 9 | selector = node_config.get('template', None) 10 | if not selector: 11 | selector = argv[0] 12 | _LOGGER.debug(" selector: %s", selector) 13 | if selector in ['lumi.sensor_86sw2', 'lumi.sensor_86sw2Un', ]: 14 | config = { 15 | 'in_cluster': [0x0000, 0x0006], 16 | 'type': 'binary_sensor', 17 | } 18 | self.add_input_cluster(0x0006) 19 | else: 20 | config = { 21 | "in_cluster": [0x0000, 0x0006, 0x0012], 22 | "type": "binary_sensor", 23 | } 24 | node_config.update(config) 25 | 26 | 27 | def _parse_attribute(entity, attrib, value, *argv, **kwargs): 28 | """ parse non standard atrributes.""" 29 | from zigpy.zcl import foundation as f 30 | _LOGGER.debug('parse %s %s %a %s', attrib, value, argv, kwargs) 31 | 32 | attributes = {} 33 | 34 | if attrib == 0xff01: 35 | attribute_name = { 36 | 4: "X-attrib-4", 37 | 1: "battery_voltage_mV", 38 | 100: "temperature", 39 | 101: "humidity", 40 | 102: "pressure", 41 | 5: "X-attrib-5", 42 | 6: "X-attrib-6", 43 | 10: "path" # was X-attrib-10 44 | } 45 | result = {} 46 | while value: 47 | skey = int(value[0]) 48 | svalue, value = f.TypeValue.deserialize(value[1:]) 49 | result[skey] = svalue.value 50 | for item, value in result.items(): 51 | key = attribute_name[item] \ 52 | if item in attribute_name else "0xff01-" + str(item) 53 | attributes[key] = value 54 | if "battery_voltage_mV" in attributes: 55 | attributes["battery_level"] = int( 56 | _battery_percent(attributes["battery_voltage_mV"])) 57 | else: 58 | result = value 59 | 60 | # if "path" in attributes: 61 | # self._entity._endpoint._device.handle_RouteRecord(attributes["path"]) 62 | if (kwargs['cluster_id'] == 6): 63 | if attrib == 0: 64 | if result == 1: 65 | event_data = { 66 | 'entity_id': entity.entity_id, 67 | 'channel': "OnOff", 68 | 'type': 'toggle', 69 | 'data': int(1), 70 | } 71 | entity.hass.bus.fire('click', event_data) 72 | if attrib == 0x8000: 73 | event_data = { 74 | 'entity_id': entity.entity_id, 75 | 'channel': "OnOff", 76 | 'type': 'toggle', 77 | 'data': result, 78 | } 79 | entity.hass.bus.fire('click', event_data) 80 | elif (kwargs['cluster_id'] == 0x0012) and attrib == 0x0055: 81 | event_data = { 82 | 'entity_id': entity.entity_id, 83 | 'channel': "MultiStateInput", 84 | } 85 | if result == 0: 86 | event_data['data'] = 'hold' 87 | elif result == 255: 88 | event_data['data'] = 'release' 89 | elif result == 1: 90 | event_data['data'] = 'single' 91 | elif result == 2: 92 | event_data['data'] = 'double' 93 | elif result == 16: 94 | event_data['data'] = 'hold' 95 | elif result == 17: 96 | event_data['data'] = 'long_released' 97 | elif result == 18: 98 | event_data['data'] = 'shake' 99 | entity.hass.bus.fire('click', event_data) 100 | 101 | attributes["last seen"] = dt_util.now() 102 | entity._device_state_attributes.update(attributes) 103 | _LOGGER.debug('updated Attributes:%s', attributes) 104 | return(attrib, result) 105 | 106 | 107 | def _battery_percent(voltage): 108 | """calculate percentage.""" 109 | min_voltage = 2750 110 | max_voltage = 3100 111 | return (voltage - min_voltage) / (max_voltage - min_voltage) * 100 112 | -------------------------------------------------------------------------------- /custom_components/device/lumi_sensor_86sw2un.py: -------------------------------------------------------------------------------- 1 | from .lumi_sensor_86sw2 import _custom_endpoint_init, _parse_attribute 2 | -------------------------------------------------------------------------------- /custom_components/device/lumi_sensor_cube.py: -------------------------------------------------------------------------------- 1 | from .lumi_sensor_cube_aqgl01 import _custom_endpoint_init, _parse_attribute 2 | -------------------------------------------------------------------------------- /custom_components/device/lumi_sensor_cube_aqgl01.py: -------------------------------------------------------------------------------- 1 | """" custom py file for device Aqara Cube.""" 2 | import logging 3 | import homeassistant.util.dt as dt_util 4 | from custom_components import zha_new 5 | 6 | _LOGGER = logging.getLogger(__name__) 7 | DOMAIN = 'cube_event' 8 | 9 | 10 | def _custom_endpoint_init(self, node_config, *argv): 11 | """set node_config based on Lumi device_type.""" 12 | config = {} 13 | selector = node_config.get('template', None) 14 | _LOGGER.debug(" selector: %s", selector) 15 | if not selector: 16 | selector = argv[0] 17 | _LOGGER.debug(" selector: %s", selector) 18 | if selector in ['lumi_sensor_cube', 'lumi_sensor_cube_aqgl01'] and self.endpoint_id == 1: 19 | config = { 20 | "in_cluster": [0x0000, 0x0003, 0x0012, 0x0019], 21 | "out_cluster": [0x0000, 0x0003, 0x0004, 0x0005, 0x0012, 0x00019], 22 | "type": "sensor", 23 | } 24 | elif selector in ['lumi_sensor_cube', 'lumi_sensor_cube_aqgl01'] and self.endpoint_id == 2: 25 | config = { 26 | "config_report": [ 27 | [0x0012, 85, 60, 3600, 5], 28 | ], 29 | "in_cluster": [0x0003, 0x0012], 30 | "out_cluster": [0x0003, 0x0004, 0x0005, 0x0012], 31 | "type": "sensor", 32 | } 33 | self.add_input_cluster(0x0012) 34 | elif selector in ['lumi_sensor_cube', 'lumi_sensor_cube_aqgl01'] and self.endpoint_id == 3: 35 | config = { 36 | "config_report": [ 37 | [0x000c, 85, 60, 3600, 5], 38 | [0x000c, 65285, 60, 3600, 5], 39 | ], 40 | "in_cluster": [0x0003, 0x000c], 41 | "out_cluster": [0x0003, 0x0004, 0x0005, 0x000c], 42 | "type": "sensor", 43 | } 44 | self.add_input_cluster(0x000c) 45 | node_config.update(config) 46 | 47 | 48 | def _parse_attribute(entity, attrib, value, *argv): 49 | """ parse non standard atrributes.""" 50 | import zigpy.types as t 51 | from zigpy.zcl import foundation as f 52 | _LOGGER.debug('parse value type %s', type(value)) 53 | result = [] 54 | if type(value) is str: 55 | result = bytearray() 56 | result.extend(map(ord, value)) 57 | value = result 58 | if entity.entity_connect == {}: 59 | entity_store = zha_new.get_entity_store(entity.hass) 60 | device_store = entity_store.get(entity._endpoint._device._ieee, {}) 61 | for dev_ent in device_store: 62 | if hasattr(dev_ent, 'cluster_key'): 63 | entity.entity_connect[dev_ent.cluster_key] = dev_ent 64 | attributes = {} 65 | if attrib == 85: 66 | attributes["value"] = float(t.Double(value)) 67 | attributes["last seen"] = dt_util.now() 68 | if isinstance(value, float): # rotation 69 | event = "rotation" 70 | command = 'cube_rotation' 71 | else: 72 | if value >= 499: 73 | event = "double_tap" 74 | elif value >= 250: 75 | event = "slide" 76 | elif value >= 100: 77 | event = "flip_180" 78 | elif value == 0: 79 | event = "shake" 80 | elif value <= 15: 81 | event = "noise" 82 | elif value > 0: 83 | event = "flip_90" 84 | command = 'cube_slide' 85 | _LOGGER.debug("value### "+str(result) + "## state ## "+event) 86 | entity._state = event 87 | event_data = { 88 | 'entity_id': entity.entity_id, 89 | 'channel': 'Level', 90 | 'command': command, 91 | 'step': event 92 | } 93 | entity.hass.bus.fire('cube_event', event_data) 94 | elif attrib == 0xff01: 95 | _LOGGER.debug("Parse dict 0xff01: set friendly attribute names") 96 | attribute_name = { 97 | 4: "X-attrib-4", 98 | 1: "battery_voltage_mV", 99 | 100: "temperature", 100 | 101: "humidity", 101 | 102: "pressure", 102 | 5: "X-attrib-5", 103 | 6: "X-attrib-6", 104 | 10: "path" # was X-attrib-10 105 | } 106 | result = {} 107 | while value: 108 | skey = int(value[0]) 109 | svalue, value = f.TypeValue.deserialize(value[1:]) 110 | result[skey] = svalue.value 111 | for item, value in result.items(): 112 | key = attribute_name[item] \ 113 | if item in attribute_name else "0xff01-" + str(item) 114 | attributes[key] = value 115 | if "battery_voltage_mV" in attributes: 116 | attributes["battery_level"] = int( 117 | _battery_percent(attributes["battery_voltage_mV"])) 118 | 119 | if "path" in attributes: 120 | entity._endpoint._device.handle_RouteRecord(attributes["path"]) 121 | entity._device_state_attributes.update(attributes) 122 | 123 | 124 | return(attrib, result) 125 | -------------------------------------------------------------------------------- /custom_components/device/lumi_sensor_ht.py: -------------------------------------------------------------------------------- 1 | from .lumi_sensor_magnet import _custom_endpoint_init, _parse_attribute 2 | -------------------------------------------------------------------------------- /custom_components/device/lumi_sensor_magnet.py: -------------------------------------------------------------------------------- 1 | """" custom py file for device.""" 2 | import logging 3 | import homeassistant.util.dt as dt_util 4 | from custom_components import zha_new 5 | 6 | _LOGGER = logging.getLogger(__name__) 7 | 8 | def _custom_endpoint_init(self, node_config, *argv): 9 | """set node_config based obn Lumi device_type.""" 10 | config = {} 11 | selector = node_config.get('template', None) 12 | if not selector: 13 | selector = argv[0] 14 | _LOGGER.debug(" selector: %s", selector) 15 | if selector in ['lumi.sensor_magnet', 'lumi.sensor_magnet.aq2']: 16 | config = { 17 | "in_cluster": [0x0000, 0x0006], 18 | "type": "binary_sensor", 19 | } 20 | self.add_input_cluster(0x0006) 21 | 22 | elif selector in ['lumi.sensor_ht', ] and self.endpoint_id == 1: 23 | config = { 24 | 'primary_cluster': 0x0405, 25 | "config_report": [ 26 | [0x0402, 0, 10, 600, 5], 27 | [0x0405, 0, 10, 600, 5], 28 | ], 29 | "in_cluster": [0x0000, 0x0402, ], # just use one sensor as main 30 | "out_cluster": [], 31 | "type": "sensor", 32 | } 33 | self.add_input_cluster(0x0402) 34 | self.add_input_cluster(0x0405) 35 | 36 | elif selector in ['lumi.weather', ] and self.endpoint_id == 1: 37 | config = { 38 | "config_report": [ 39 | [0x0402, 0, 10, 120, 5], 40 | [0x0403, 0, 10, 120, 5], 41 | [0x0405, 0, 10, 120, 5], 42 | ], 43 | "in_cluster": [0x0000, 0x0402], # just use one sensor as main 44 | "out_cluster": [], 45 | "type": "sensor", 46 | } 47 | self.add_input_cluster(0x0402) 48 | self.add_input_cluster(0x0403) 49 | self.add_input_cluster(0x0405) 50 | elif selector in ['lumi.sensor_motion', ]: 51 | config = { 52 | "config_report": [ 53 | [0x0406, 0, 10, 1800, 1], 54 | ], 55 | "in_cluster": [0x0000, 0xffff, 0x0406], 56 | "out_cluster": [], 57 | "type": "binary_sensor", 58 | } 59 | self.add_input_cluster(0x0406) # cluster_not in endpoint clusters 60 | elif selector in ['lumi.sensor_motion.aq2', ]: 61 | config = { 62 | 'primary_cluster': 0x406, 63 | "config_report": [ 64 | [0x0406, 0, 10, 1800, 1], 65 | [0x0400, 0, 10, 1800, 10], 66 | ], 67 | "in_cluster": [0x0000, 0x0406, 0xffff], 68 | "out_cluster": [], 69 | # "type": "binary_sensor", 70 | } 71 | self.add_input_cluster(0x0406) 72 | self.add_input_cluster(0x0400) 73 | elif selector == 'lumi.sensor_wleak.aq1': 74 | config = { 75 | "in_cluster": [0x0000, 0xff01, 0x0500], 76 | "out_cluster": [0x0500], 77 | "type": "binary_sensor", 78 | "config_report": [ 79 | [0xff01, 0, 10, 1800, 1], 80 | ], 81 | } 82 | self.add_input_cluster(0x0500) 83 | self.add_output_cluster(0x0500) 84 | elif selector == 'lumi.vibration.aq1' and self.endpoint_id == 1: 85 | config = { 86 | "type": "binary_sensor", 87 | "in_cluster": [0x0000, 0x0101] 88 | } 89 | # asyncio.ensure_future(zha_new.discover_cluster_values(self, self.in_clusters[0x0101])) 90 | node_config.update(config) 91 | 92 | 93 | def _battery_percent(voltage): 94 | """calculate percentage.""" 95 | min_voltage = 2750 96 | max_voltage = 3100 97 | return (voltage - min_voltage) / (max_voltage - min_voltage) * 100 98 | 99 | 100 | def _parse_attribute(entity, attrib, value, *argv, **kwargs): 101 | """ parse non standard atrributes.""" 102 | import zigpy.types as t 103 | from zigpy.zcl import foundation as f 104 | _LOGGER.debug('parse %s %s %a %s', attrib, value, argv, kwargs) 105 | # if type(value) is str: 106 | # result = bytearray() 107 | # result.extend(map(ord, value)) 108 | # value = result 109 | 110 | if entity.entity_connect == {}: 111 | entity_store = zha_new.get_entity_store(entity.hass) 112 | device_store = entity_store.get(entity._endpoint._device._ieee, {}) 113 | for dev_ent in device_store: 114 | if hasattr(dev_ent, 'cluster_key'): 115 | entity.entity_connect[dev_ent.cluster_key] = dev_ent 116 | 117 | attributes = {} 118 | if attrib == 0xff02: 119 | # _LOGGER.debug("Parse dict 0xff02: set friendly attribute names") 120 | attribute_name = ("state", "battery_voltage_mV", 121 | "val3", "val4", 122 | "val5", "val6") 123 | result = [] 124 | for svalue in value: 125 | result.append(svalue.value) 126 | attributes = dict(zip(attribute_name, result)) 127 | 128 | if "battery_voltage_mV" in attributes: 129 | attributes["battery_level"] = int( 130 | _battery_percent(attributes["battery_voltage_mV"])) 131 | 132 | elif attrib == 0xff01: 133 | _LOGGER.debug("Parse dict 0xff01: set friendly attribute names") 134 | attribute_name = { 135 | 4: "X-attrib-4", 136 | 1: "battery_voltage_mV", 137 | 100: "temperature", 138 | 101: "humidity", 139 | 102: "pressure", 140 | 5: "X-attrib-5", 141 | 6: "X-attrib-6", 142 | 10: "path" # was X-attrib-10 143 | } 144 | result = {} 145 | while value: 146 | skey = int(value[0]) 147 | svalue, value = f.TypeValue.deserialize(value[1:]) 148 | result[skey] = svalue.value 149 | for item, value in result.items(): 150 | key = attribute_name[item] \ 151 | if item in attribute_name else "0xff01-" + str(item) 152 | attributes[key] = value 153 | if "battery_voltage_mV" in attributes: 154 | attributes["battery_level"] = int( 155 | _battery_percent(attributes["battery_voltage_mV"])) 156 | elif attrib == 43041: 157 | attribute_name = ("X-attrib-val1", 158 | "X-attrib-val2", 159 | "X-attrib-val3") 160 | result = [] 161 | svalue, value = t.uint40_t.deserialize(value) 162 | result.append(svalue) 163 | svalue, value = f.TypeValue.deserialize(value) 164 | result.append(svalue.value) 165 | svalue, value = f.TypeValue.deserialize(value) 166 | result.append(svalue.value) 167 | attributes = dict(zip(attribute_name, result)) 168 | else: 169 | result = value 170 | _LOGGER.debug("Parse Result: %s", result) 171 | for attr in ("temperature", "humidity"): 172 | if attr in attributes and attr in entity.entity_connect: 173 | entity.entity_connect[attr]._state = attributes[attr] 174 | if "pressure" in attributes: 175 | entity.entity_connect["pressure"]._state = round( 176 | float(attributes["pressure"]) / 100, 0) 177 | 178 | attributes["last seen"] = dt_util.now() 179 | if "path" in attributes: 180 | entity._endpoint._device.handle_RouteRecord(attributes["path"]) 181 | 182 | if entity._model == 'lumi.vibration.aq1': 183 | if attrib == 85: 184 | event_data = { 185 | 'entity_id': entity.entity_id, 186 | 'channel': "alarm", 187 | 'type': "vibration" if value == 1 else ("tilt" if value == 2 else "drop") 188 | } 189 | entity.hass.bus.fire('alarm', event_data) 190 | attributes['alarm'] = event_data['type'] 191 | elif attrib == 1283: 192 | _LOGGER.debug("Rotation: %s", value) 193 | attributes['rotation'] = value 194 | elif attrib == 1288: 195 | angle_z = value & 0x0fff 196 | if angle_z > 2048: 197 | angle_z -= 4096 198 | angle_y = (value >> 16) & 0x0fff 199 | if angle_y > 2048: 200 | angle_y -= 4096 201 | angle_x = (value >> 32) & 0x0fff 202 | if angle_x > 2048: 203 | angle_x -= 4096 204 | _LOGGER.debug("Attrib 0x%04x: 0x%04x : %s %s %s", attrib, value, angle_x, angle_y, angle_z) 205 | attributes['angle_x'] = angle_x 206 | attributes['angle_y'] = angle_y 207 | attributes['angle_z'] = angle_z 208 | elif entity._model in ['lumi.sensor_magnet.aq2', 'lumi.sensor_wleak.aq1']: 209 | if "temperature" in attributes: 210 | entity._state = attributes["temperature"] 211 | 212 | entity._device_state_attributes.update(attributes) 213 | return(attrib, result) 214 | -------------------------------------------------------------------------------- /custom_components/device/lumi_sensor_magnet_aq2.py: -------------------------------------------------------------------------------- 1 | from .lumi_sensor_magnet import _custom_endpoint_init, _parse_attribute 2 | -------------------------------------------------------------------------------- /custom_components/device/lumi_sensor_motion.py: -------------------------------------------------------------------------------- 1 | from .lumi_sensor_magnet import _custom_endpoint_init, _parse_attribute 2 | -------------------------------------------------------------------------------- /custom_components/device/lumi_sensor_motion_aq2.py: -------------------------------------------------------------------------------- 1 | from .lumi_sensor_magnet import _custom_endpoint_init, _parse_attribute 2 | -------------------------------------------------------------------------------- /custom_components/device/lumi_sensor_switch.py: -------------------------------------------------------------------------------- 1 | """" custom py file for device.""" 2 | import logging 3 | import homeassistant.util.dt as dt_util 4 | 5 | _LOGGER = logging.getLogger(__name__) 6 | 7 | 8 | def _custom_endpoint_init(self, node_config, *argv): 9 | selector = node_config.get('template', None) 10 | if not selector: 11 | selector = argv[0] 12 | _LOGGER.debug(" selector: %s", selector) 13 | 14 | config = { 15 | 'type': 'binary_sensor', 16 | } 17 | self.add_input_cluster(0x0006) 18 | 19 | node_config.update(config) 20 | 21 | 22 | def _parse_attribute(entity, attrib, value, *argv, **kwargs): 23 | """ parse non standard atrributes.""" 24 | from zigpy.zcl import foundation as f 25 | _LOGGER.debug('parse %s %s %a %s', attrib, value, argv, kwargs) 26 | attributes = {} 27 | 28 | if attrib == 0xff01: 29 | attribute_name = { 30 | 4: "X-attrib-4", 31 | 1: "battery_voltage_mV", 32 | 100: "temperature", 33 | 101: "humidity", 34 | 102: "pressure", 35 | 5: "X-attrib-5", 36 | 6: "X-attrib-6", 37 | 10: "path" # was X-attrib-10 38 | } 39 | result = {} 40 | while value: 41 | skey = int(value[0]) 42 | svalue, value = f.TypeValue.deserialize(value[1:]) 43 | result[skey] = svalue.value 44 | for item, value in result.items(): 45 | key = attribute_name[item] \ 46 | if item in attribute_name else "0xff01-" + str(item) 47 | attributes[key] = value 48 | if "battery_voltage_mV" in attributes: 49 | attributes["battery_level"] = int( 50 | _battery_percent(attributes["battery_voltage_mV"])) 51 | else: 52 | result = value 53 | 54 | # if "path" in attributes: 55 | # self._entity._endpoint._device.handle_RouteRecord(attributes["path"]) 56 | 57 | if (kwargs['cluster_id'] == 0x0006) and (attrib == 0x0000): 58 | event_data = { 59 | 'entity_id': entity.entity_id, 60 | 'channel': "MultiStateInput", 61 | } 62 | if result == 0: 63 | event_data['data'] = 'hold' 64 | elif result == 255: 65 | event_data['data'] = 'release' 66 | elif result == 1: 67 | event_data['data'] = 'single' 68 | elif result == 2: 69 | event_data['data'] = 'double' 70 | elif result == 3: 71 | event_data['data'] = 'tripple' 72 | elif result == 4: 73 | event_data['data'] = 'quad' 74 | elif result == 16: 75 | event_data['data'] = 'hold' 76 | elif result == 17: 77 | event_data['data'] = 'long_released' 78 | elif result == 18: 79 | event_data['data'] = 'shake' 80 | entity.hass.bus.fire('click', event_data) 81 | 82 | attributes["last seen"] = dt_util.now() 83 | entity._device_state_attributes.update(attributes) 84 | _LOGGER.debug('updated Attributes:%s', attributes) 85 | return(attrib, result) 86 | 87 | 88 | def _battery_percent(voltage): 89 | """calculate percentage.""" 90 | min_voltage = 2750 91 | max_voltage = 3100 92 | return (voltage - min_voltage) / (max_voltage - min_voltage) * 100 93 | -------------------------------------------------------------------------------- /custom_components/device/lumi_sensor_switch_aq2.py: -------------------------------------------------------------------------------- 1 | from .lumi_sensor_86sw2 import _custom_endpoint_init, _parse_attribute 2 | -------------------------------------------------------------------------------- /custom_components/device/lumi_sensor_switch_aq3.py: -------------------------------------------------------------------------------- 1 | from .lumi_sensor_86sw2 import _custom_endpoint_init, _parse_attribute 2 | -------------------------------------------------------------------------------- /custom_components/device/lumi_sensor_wleak_aq1.py: -------------------------------------------------------------------------------- 1 | from .lumi_sensor_magnet import _custom_endpoint_init, _parse_attribute 2 | -------------------------------------------------------------------------------- /custom_components/device/lumi_vibration_aq1.py: -------------------------------------------------------------------------------- 1 | from .lumi_sensor_magnet import _custom_endpoint_init, _parse_attribute 2 | -------------------------------------------------------------------------------- /custom_components/device/lumi_weather.py: -------------------------------------------------------------------------------- 1 | from .lumi_sensor_magnet import _custom_endpoint_init, _parse_attribute 2 | -------------------------------------------------------------------------------- /custom_components/device/multi.py: -------------------------------------------------------------------------------- 1 | """" custom py file for device.""" 2 | import logging 3 | import homeassistant.util.dt as dt_util 4 | from custom_components import zha_new 5 | import zigpy.types as t 6 | _LOGGER = logging.getLogger(__name__) 7 | 8 | 9 | def _custom_endpoint_init(self, node_config, *argv): 10 | """set node_config based obn Lumi device_type.""" 11 | config = {} 12 | selector = node_config.get('template', None) 13 | if not selector: 14 | selector = argv[0] 15 | _LOGGER.debug(" selector: %s", selector) 16 | config = { 17 | "config_report": [ 18 | [0xfc02, 0x0010, 1, 1800, t.uint8_t(1), 0x1241], 19 | [0xfc02, 0x0012, 1, 1800, t.uint16_t(1), 0x1241], 20 | [0xfc02, 0x0013, 1, 1800, t.uint16_t(1), 0x1241], 21 | [0xfc02, 0x0014, 1, 1800, t.uint16_t(1), 0x1241], 22 | ], 23 | "in_cluster": [0x0000, 0x0402, 0x0500, 0xfc02], 24 | "out_cluster": [], 25 | "type": "binary_sensor", 26 | } 27 | node_config.update(config) 28 | 29 | 30 | def _parse_attribute(entity, attrib, value, *argv, **kwargs): 31 | """ parse non standard atrributes.""" 32 | _LOGGER.debug('parse %s %s %a %s', attrib, value, argv, kwargs) 33 | cluster_id = kwargs.get('cluster_id', None) 34 | attributes = dict() 35 | if entity.entity_connect == {}: 36 | entity_store = zha_new.get_entity_store(entity.hass) 37 | device_store = entity_store.get(entity._endpoint._device._ieee, {}) 38 | for dev_ent in device_store: 39 | if hasattr(dev_ent, 'cluster_key'): 40 | entity.entity_connect[dev_ent.cluster_key] = dev_ent 41 | if cluster_id == 0x0402: 42 | if attrib == 0: 43 | attributes['Temperature'] = value 44 | elif cluster_id == 0xfc02: 45 | if attrib == 0x0010: 46 | command = "move" 47 | elif attrib == 0x0012: 48 | command = "X-Axis" 49 | elif attrib == 0x0013: 50 | command = "Y-Axis" 51 | elif attrib == 0x0014: 52 | command = "Z-Axis" 53 | attributes['last alarm'] = dt_util.now() 54 | event_data = { 55 | 'entity_id': entity.entity_id, 56 | 'channel': "alarm", 57 | 'type': command, 58 | 'value': value, 59 | } 60 | entity.hass.bus.fire('alarm', event_data) 61 | entity._device_state_attributes.update(attributes) 62 | return(attrib, value) 63 | -------------------------------------------------------------------------------- /custom_components/device/plug_01.py: -------------------------------------------------------------------------------- 1 | """" custom py file for device.""" 2 | import logging 3 | _LOGGER = logging.getLogger(__name__) 4 | 5 | 6 | def _custom_endpoint_init(self, node_config, *argv): 7 | """set node_config.""" 8 | 9 | self.profile_id = 260 10 | if self.device_type == 0x0010: 11 | self.device_type = 0x0051 12 | config = { 13 | "config_report": [ 14 | [6, 0, 0, 60, 1], 15 | ], 16 | "in_cluster": [0x0000, 0x0006], 17 | "out_cluster": [], 18 | } 19 | node_config.update(config) 20 | 21 | def _parse_attribute(entity, attrib, value, *argv, **kwargs): 22 | """parse non standard attributes.""" 23 | return(attrib, value) 24 | 25 | -------------------------------------------------------------------------------- /custom_components/device/sp31.py: -------------------------------------------------------------------------------- 1 | """" custom py file for device SP31 from net2grid.""" 2 | import logging 3 | _LOGGER = logging.getLogger(__name__) 4 | 5 | 6 | def _custom_endpoint_init(self, node_config, *argv): 7 | """set node_config.""" 8 | config = {} 9 | if self._endpoint_id == 10: # smartenergy metering 10 | config = { 11 | "in_cluster": [0x0702, ], 12 | "config_report": [ 13 | [0x0702, 0, 5, 180, 1], 14 | ], 15 | "type": "sensor", 16 | } 17 | node_config.update(config) 18 | if self._endpoint_id == 1: 19 | config = { 20 | "in_cluster": [0x0000, 0x0006], 21 | "out_cluster": [], 22 | "config_report": [ 23 | [0x0006, 0, 1, 180, 1], 24 | ], 25 | "type": "switch", 26 | } 27 | node_config.update(config) 28 | 29 | 30 | def _parse_attribute(entity, attrib, value, *argv): 31 | """parse non standard attributes.""" 32 | return(attrib, value) 33 | -------------------------------------------------------------------------------- /custom_components/device/tradfri_remote_control.py: -------------------------------------------------------------------------------- 1 | """ tradfri TRADFRI remote template.""" 2 | 3 | 4 | def _custom_cluster_command(self, tsn, command_id, args): 5 | value = self._brightness 6 | self._state = 1 7 | if command_id == 5: 8 | _up_down = 1 9 | elif command_id == 1: 10 | _up_down = -1 11 | elif command_id == 7: 12 | return 13 | 14 | if args[1] == 70: 15 | value = value + (16 * _up_down) 16 | elif args[1] == 195: 17 | value = value + (32 * _up_down) 18 | if value > 255: 19 | self._brightness = 255 20 | elif value <= 0: 21 | self._state = 0 22 | self._brightness = 0 23 | else: 24 | self._brightness = value 25 | self.schedule_update_ha_state() 26 | -------------------------------------------------------------------------------- /custom_components/device/tradfri_wireless_dimmer.py: -------------------------------------------------------------------------------- 1 | """ tradfri dimmer template.""" 2 | import logging 3 | _LOGGER = logging.getLogger(__name__) 4 | 5 | def _custom_endpoint_init(self, node_config, *argv): 6 | """set node_config based obn Lumi device_type.""" 7 | config = {} 8 | selector = node_config.get('template', None) 9 | if not selector: 10 | selector = argv[0] 11 | _LOGGER.debug(" selector: %s", selector) 12 | config = { 13 | "config_report": [ 14 | [0x0001, 0x0020, 60, 600, 5], 15 | [0x0001, 0x0021, 60, 600, 5] 16 | ], 17 | "in_cluster": [0x0000, 0x0001, ], 18 | "type": "binary_sensor", 19 | } 20 | node_config.update(config) 21 | 22 | 23 | def _custom_cluster_command(self, tsn, command_id, args): 24 | pass 25 | -------------------------------------------------------------------------------- /custom_components/zha_new/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Support for ZigBee Home Automation devices. 3 | 4 | For more details about this component, please refer to the documentation at 5 | https://home-assistant.io/components/zha/ 6 | Revision: 0.81.100 7 | """ 8 | 9 | from importlib import import_module 10 | import asyncio 11 | import logging 12 | _LOGGER = logging.getLogger(__name__) 13 | _LOGGER.debug("start zha_new") 14 | import voluptuous as vol 15 | from homeassistant.helpers.event import async_track_point_in_time 16 | import homeassistant.util.dt as dt_util 17 | import datetime 18 | import homeassistant.helpers.config_validation as cv 19 | from homeassistant import const as ha_const 20 | from homeassistant.helpers import discovery, entity 21 | from homeassistant.helpers.entity_component import EntityComponent 22 | from homeassistant.util import slugify 23 | from homeassistant.helpers.restore_state import RestoreEntity 24 | from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE 25 | from .const import * 26 | #from .helpers import create_MC_Entity 27 | 28 | REQUIREMENTS = [ 29 | # 'https://github.com/Yoda-x/bellows/archive/master.zip#bellows==100.7.4.9', 30 | 'https://github.com/Yoda-x/bellows/archive/master.zip#bellows==100.7.4.12', 31 | 'https://github.com/Yoda-x/zigpy/archive/master.zip#zigpy==100.1.4.10', 32 | # 'https://github.com/Yoda-x/zigpy/archive/master.zip#zigpy==100.1.4.7', 33 | ] 34 | 35 | 36 | def set_entity_store(hass, entity_store): 37 | all_discovery_info = hass.data.get(DISCOVERY_KEY, {}) 38 | all_discovery_info[ENTITY_STORE] = entity_store 39 | 40 | 41 | def get_entity_store(hass): 42 | all_discovery_info = hass.data.get(DISCOVERY_KEY, {}) 43 | return all_discovery_info[ENTITY_STORE] 44 | 45 | 46 | def populate_data(): 47 | """Populate data using constants from bellows. 48 | 49 | These cannot be module level, as importing bellows must be done in a 50 | in a function. 51 | 52 | """ 53 | 54 | from zigpy import zcl 55 | from zigpy.profiles import PROFILES, zha, zll 56 | 57 | DEVICE_CLASS[zha.PROFILE_ID] = { 58 | zha.DeviceType.ON_OFF_SWITCH: 'switch', 59 | zha.DeviceType.SMART_PLUG: 'switch', 60 | zha.DeviceType.MAIN_POWER_OUTLET: 'switch', 61 | zha.DeviceType.ON_OFF_LIGHT: 'light', 62 | zha.DeviceType.DIMMABLE_LIGHT: 'light', 63 | zha.DeviceType.COLOR_DIMMABLE_LIGHT: 'light', 64 | zha.DeviceType.ON_OFF_LIGHT_SWITCH: 'binary_sensor', 65 | zha.DeviceType.DIMMER_SWITCH: 'binary_sensor', 66 | zha.DeviceType.COLOR_DIMMER_SWITCH: 'binary_sensor', 67 | zha.DeviceType.COLOR_SCENE_CONTROLLER: 'binary_sensor', 68 | zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor', 69 | zha.DeviceType.LEVEL_CONTROL_SWITCH: 'binary_sensor', 70 | zha.DeviceType.REMOTE_CONTROL: 'binary_sensor', 71 | zha.DeviceType.OCCUPANCY_SENSOR: 'binary_sensor', 72 | zha.DeviceType.IAS_ZONE: 'binary_sensor', 73 | zha.DeviceType.LIGHT_SENSOR: 'binary_sensor', 74 | zha.DeviceType.ON_OFF_BALLAST: 'switch', 75 | zha.DeviceType.ON_OFF_PLUG_IN_UNIT: 'switch', 76 | zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: 'switch', 77 | zha.DeviceType.COLOR_TEMPERATURE_LIGHT: 'light', 78 | zha.DeviceType.EXTENTED_COLOR_LIGHT: 'light', 79 | zha.DeviceType.LIGHT_LEVEL_SENSOR: 'binary_sensor', 80 | zha.DeviceType.NON_COLOR_CONTROLLER: 'binary_sensor', 81 | zha.DeviceType.NON_COLOR_SCENE_CONTROLLER: 'binary_sensor', 82 | zha.DeviceType.CONTROL_BRIDGE: 'binary_sensor', 83 | zha.DeviceType.ON_OFF_SENSOR: 'binary_sensor', 84 | 85 | } 86 | 87 | DEVICE_CLASS[zll.PROFILE_ID] = { 88 | zll.DeviceType.ON_OFF_LIGHT: 'light', 89 | zll.DeviceType.ON_OFF_PLUGIN_UNIT: 'switch', 90 | zll.DeviceType.DIMMABLE_LIGHT: 'light', 91 | zll.DeviceType.DIMMABLE_PLUGIN_UNIT: 'light', 92 | zll.DeviceType.COLOR_LIGHT: 'light', 93 | zll.DeviceType.EXTENDED_COLOR_LIGHT: 'light', 94 | zll.DeviceType.COLOR_TEMPERATURE_LIGHT: 'light', 95 | zll.DeviceType.COLOR_SCENE_CONTROLLER: 'binary_sensor', 96 | zll.DeviceType.SCENE_CONTROLLER: 'binary_sensor', 97 | zll.DeviceType.CONTROLLER: 'binary_sensor', 98 | zll.DeviceType.COLOR_CONTROLLER: 'binary_sensor', 99 | zll.DeviceType.ON_OFF_SENSOR: 'binary_sensor', 100 | } 101 | 102 | SINGLE_CLUSTER_DEVICE_CLASS.update({ 103 | zcl.clusters.general.OnOff: 'switch', 104 | zcl.clusters.measurement.TemperatureMeasurement: 'sensor', 105 | zcl.clusters.measurement.RelativeHumidity: 'sensor', 106 | zcl.clusters.measurement.PressureMeasurement: 'sensor', 107 | zcl.clusters.measurement.IlluminanceMeasurement: 'sensor', 108 | zcl.clusters.measurement.OccupancySensing: 'binary_sensor', 109 | zcl.clusters.homeautomation.ElectricalMeasurement: 'sensor', 110 | }) 111 | 112 | # A map of hass components to all Zigbee clusters it could use 113 | for profile_id, classes in DEVICE_CLASS.items(): 114 | profile = PROFILES[profile_id] 115 | for device_type, component in classes.items(): 116 | if component not in COMPONENT_CLUSTERS: 117 | COMPONENT_CLUSTERS[component] = (set(), set()) 118 | clusters = profile.CLUSTERS[device_type] 119 | COMPONENT_CLUSTERS[component][0].update(clusters[0]) 120 | COMPONENT_CLUSTERS[component][1].update(clusters[1]) 121 | """end populate_data """ 122 | 123 | 124 | """ Schema for configurable options in configuration.yaml """ 125 | DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({ 126 | vol.Optional(ha_const.CONF_TYPE): cv.string, 127 | vol.Optional(CONF_IN_CLUSTER): cv.ensure_list, 128 | vol.Optional(CONF_OUT_CLUSTER): cv.ensure_list, 129 | vol.Optional(CONF_CONFIG_REPORT): cv.ensure_list, 130 | vol.Optional(CONF_MODEL): cv.string, 131 | vol.Optional(CONF_MANUFACTURER): cv.string, 132 | vol.Optional(CONF_TEMPLATE): cv.string, 133 | }) 134 | 135 | CONFIG_SCHEMA = vol.Schema({ 136 | DOMAIN: vol.Schema({ 137 | CONF_USB_PATH: cv.string, 138 | vol.Optional(CONF_BAUDRATE, default=57600): cv.positive_int, 139 | CONF_DATABASE: cv.string, 140 | vol.Optional(CONF_DEVICE_CONFIG, default={}): 141 | vol.Schema({cv.string: DEVICE_CONFIG_SCHEMA_ENTRY}), 142 | }) 143 | }, extra=vol.ALLOW_EXTRA) 144 | 145 | def _custom_endpoint_init(self, node_config, *argv): 146 | pass 147 | 148 | 149 | 150 | 151 | 152 | async def async_setup(hass, config): 153 | 154 | 155 | global APPLICATION_CONTROLLER 156 | import bellows.ezsp 157 | from bellows.zigbee.application import ControllerApplication 158 | _LOGGER.debug("async_setup zha_new") 159 | ezsp_ = bellows.ezsp.EZSP() 160 | usb_path = config[DOMAIN].get(CONF_USB_PATH) 161 | baudrate = config[DOMAIN].get(CONF_BAUDRATE) 162 | await ezsp_.connect(usb_path, baudrate) 163 | 164 | database = config[DOMAIN].get(CONF_DATABASE) 165 | APPLICATION_CONTROLLER = ControllerApplication(ezsp_, database) 166 | listener = ApplicationListener(hass, config) 167 | APPLICATION_CONTROLLER.add_listener(listener) 168 | await APPLICATION_CONTROLLER.startup(auto_form=True) 169 | 170 | listener.component = component = EntityComponent(_LOGGER, DOMAIN, hass, datetime.timedelta(minutes=1)) 171 | zha_controller = zha_state(hass, ezsp_, APPLICATION_CONTROLLER, 'controller', 'Init') 172 | listener.controller = zha_controller 173 | listener.registry = await hass.helpers.device_registry.async_get_registry() 174 | await component.async_add_entities([zha_controller]) 175 | zha_controller.async_schedule_update_ha_state() 176 | # await asyncio.sleep(5) 177 | for device in APPLICATION_CONTROLLER.devices.values(): 178 | hass.async_add_job(listener.async_device_initialized(device, False)) 179 | await asyncio.sleep(0.1) 180 | 181 | async def permit(service): 182 | """Allow devices to join this network.""" 183 | duration = service.data.get(ATTR_DURATION) 184 | _LOGGER.info("Permitting joins for %ss", duration) 185 | zha_controller._state = 'Permit' 186 | zha_controller.async_schedule_update_ha_state() 187 | await APPLICATION_CONTROLLER.permit(duration) 188 | 189 | async def _async_clear_state(entity): 190 | if entity._state == 'Permit': 191 | entity._state = 'Run' 192 | entity.async_schedule_update_ha_state() 193 | 194 | async_track_point_in_time( 195 | zha_controller.hass, _async_clear_state(zha_controller), 196 | dt_util.utcnow() + datetime.timedelta(seconds=duration)) 197 | 198 | hass.services.async_register(DOMAIN, SERVICE_PERMIT, permit, 199 | schema=SERVICE_SCHEMAS[SERVICE_PERMIT]) 200 | 201 | async def remove(service): 202 | """remove device from the network""" 203 | ieee_list = [] 204 | ieee = service.data.get(ATTR_IEEE) 205 | nwk = service.data.get(ATTR_NWKID) 206 | if ieee == '' and nwk is None: 207 | _LOGGER.debug("service remove device str empty") 208 | return 209 | _LOGGER.debug("service remove device str: %s", ieee if ieee else nwk) 210 | for device in APPLICATION_CONTROLLER.devices.values(): 211 | if (ieee in str(device._ieee) and ieee != '') or nwk == device.nwk: 212 | ieee_list.append(device.ieee) 213 | for device in ieee_list: 214 | await APPLICATION_CONTROLLER.remove(device) 215 | 216 | hass.services.async_register(DOMAIN, SERVICE_REMOVE, remove, 217 | schema=SERVICE_SCHEMAS[SERVICE_REMOVE]) 218 | 219 | async def command(service): 220 | listener.command(service.data) 221 | # hass.services.async_register(DOMAIN, SERVICE_COMMAND, command, 222 | # schema=SERVICE_SCHEMAS[SERVICE_COMMAND]) 223 | 224 | async def mc_command(service): 225 | listener.mc_command(service.data) 226 | hass.services.async_register(DOMAIN, SERVICE_MC_COMMAND, mc_command, 227 | schema=SERVICE_SCHEMAS[SERVICE_MC_COMMAND]) 228 | 229 | async def async_handle_light_step_up_service(service, *args, **kwargs): 230 | _LOGGER.debug("called service light_step_up %s %s", args, kwargs) 231 | return 232 | 233 | hass.services.async_register( 234 | DOMAIN, SERVICE_COLORTEMP_STEP_UP, async_handle_light_step_up_service, 235 | schema=SERVICE_SCHEMAS[SERVICE_COLORTEMP_STEP]) 236 | 237 | zha_controller._state = "Run" 238 | zha_controller.async_schedule_update_ha_state() 239 | return True 240 | 241 | 242 | class ApplicationListener: 243 | 244 | """All handlers for events that happen on the ZigBee application.""" 245 | 246 | def __init__(self, hass, config): 247 | """Initialize the listener.""" 248 | self._hass = hass 249 | self._config = config 250 | self.registry = None 251 | hass.data[DISCOVERY_KEY] = hass.data.get(DISCOVERY_KEY, {}) 252 | hass.data[DISCOVERY_KEY][ENTITY_STORE] = hass.data[DISCOVERY_KEY].get( 253 | ENTITY_STORE, 254 | {}) 255 | self.controller = None 256 | self._entity_list = dict() 257 | self.device_store = [] 258 | self.mc_subscribers = {} 259 | self.custom_devices = {} 260 | self._groups = set() 261 | 262 | def device_updated(self, device): 263 | pass 264 | 265 | async def mc_command(data): 266 | 267 | 268 | 269 | return 270 | 271 | def subscribe_group(self, group_id): 272 | # keeps a list of susbcribers, 273 | # forwardrequest to zigpy if a group is new, otherwise do nothing 274 | _LOGGER.debug("received subscribe group: %s", group_id) 275 | if group_id in self._groups: 276 | return 277 | self._hass.async_add_job( 278 | APPLICATION_CONTROLLER.subscribe_group(group_id)) 279 | #create dummy device 280 | 281 | # mdev = z.device.Device(self, 0, group_id) 282 | # mdev.add_endpoint(1) 283 | # mdev.endpoint[1].profile = z.profiles.zha.PROFILE_ID 284 | # for cluster_id in (0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0300): 285 | # mdev.endpoint[1].in_clusters[cluster_id] = cluster \ 286 | # = z.zcl.Cluster.from_id( 287 | # mdev.add_endpoint[1], 288 | # cluster_id 289 | # ) 290 | # if hasattr(cluster, 'ep_attribute'): 291 | # mdev.endpoint[1]._cluster_attr[cluster.ep_attribute] = cluster 292 | # discovery_info = { 293 | # 'device': mdev, 294 | # 'group_id': group_id} 295 | # entity = MEntity(discovery_info) 296 | self._groups.add(group_id) 297 | 298 | 299 | 300 | def unsubscribe_group(self, group_id): 301 | # keeps a list of susbcribers, 302 | # forwardrequest to zigpy if last subscriber is gone, otherwise do nothing 303 | self._hass.async_add_job( 304 | APPLICATION_CONTROLLER.unsubscribe_group(group_id)) 305 | 306 | def device_removed(self, device): 307 | """Remove related entities from HASS.""" 308 | entity_store = get_entity_store(self._hass) 309 | _LOGGER.debug("remove %s", device._ieee) 310 | _LOGGER.debug("No of entities:%s", len(entity_store)) 311 | if device._ieee in entity_store: 312 | for dev_ent in entity_store[device._ieee]: 313 | _LOGGER.debug("remove entity %s", dev_ent.entity_id) 314 | # self._entity_list.pop(dev_ent.entity_id, None) 315 | self._hass.async_add_job(dev_ent.async_remove()) 316 | entity_store.pop(device._ieee) 317 | # cleanup Discovery_Key 318 | for dev_ent in list(self._hass.data[DISCOVERY_KEY]): 319 | if str(device._ieee) in dev_ent: 320 | self._hass.data[DISCOVERY_KEY].pop(dev_ent) 321 | 322 | def device_joined(self, device): 323 | # Wait for device_initialized, instead 324 | self.controller._state = 'Joined ' + str(device._ieee) 325 | self.controller.async_schedule_update_ha_state() 326 | _LOGGER.debug("Device joined: %s:", device._ieee) 327 | 328 | def device_announce(self, device): 329 | """if a device rej/oines the network, eg switched on again.""" 330 | _LOGGER.debug("Device announced: %s:", device._ieee) 331 | """find the device entities and get updates""" 332 | 333 | def device_initialized(self, device): 334 | """Handle device joined and basic information discovered.""" 335 | self.controller._state = 'Device init ' + str(device._ieee) 336 | self.controller.async_schedule_update_ha_state() 337 | _LOGGER.debug("Device initialized: %s:", device._ieee) 338 | self._hass.async_add_job(self.async_device_initialized(device, True)) 339 | 340 | def device_left(self, device): 341 | self.controller._state = 'Left ' + str(device._ieee) 342 | self.controller.async_schedule_update_ha_state() 343 | 344 | async def _async_clear_state(entity): 345 | entity._state = 'Run' 346 | entity.async_schedule_update_ha_state() 347 | async_track_point_in_time( 348 | self.controller.hass, _async_clear_state(self.controller), 349 | dt_util.utcnow() + datetime.timedelta(seconds=5)) 350 | 351 | async def async_device_initialized(self, device, join): 352 | """Handle device joined and basic information discovered (async).""" 353 | import zigpy.profiles 354 | populate_data() 355 | discovered_info = {} 356 | out_clusters = [] 357 | primary_cluster = None 358 | # loop over endpoints 359 | 360 | if join: 361 | for endpoint_id, endpoint in device.endpoints.items(): 362 | if endpoint_id == 0: # ZDO 363 | continue 364 | if 0 in endpoint.in_clusters: 365 | discovered_info = await _discover_endpoint_info(endpoint) 366 | device.model = device.model if device.model else discovered_info.get(CONF_MODEL, device.model) 367 | device.manufacturer = discovered_info.get(CONF_MANUFACTURER, device.manufacturer) 368 | _LOGGER.debug("[0x%04x] device init for %s(%s)(%s) -> Endpoints: %s, %s ", 369 | device.nwk, type(device.model), device.model, device.ieee, list(device.endpoints.keys()), 370 | "new join" if join else "already joined") 371 | for endpoint_id, endpoint in device.endpoints.items(): 372 | _LOGGER.debug("[0x%04x:%s] endpoint init", device.nwk, endpoint_id, ) 373 | if endpoint_id == 0: # ZDO 374 | continue 375 | 376 | component = None 377 | profile_clusters = [set(), set()] 378 | device_key = '%s-%s' % (str(device.ieee), endpoint_id) 379 | node_config = self._config[DOMAIN][CONF_DEVICE_CONFIG].get(device_key, {}) 380 | _LOGGER.debug("[0x%04x:%s] node config for %s: %s", 381 | device.nwk, 382 | endpoint_id, 383 | device_key, 384 | node_config) 385 | 386 | if CONF_TEMPLATE in node_config: 387 | device.model = node_config.get(CONF_TEMPLATE, "default") 388 | if device.model not in self.custom_devices: 389 | self.custom_devices[device.model] = custom_module = get_custom_device_info(device.model) 390 | else: 391 | custom_module = self.custom_devices[device.model] 392 | if '_custom_endpoint_init' in custom_module: 393 | custom_module['_custom_endpoint_init'](endpoint, node_config, device.model) 394 | 395 | discovered_info = {CONF_MODEL: device.model, 396 | CONF_MANUFACTURER: device.manufacturer} 397 | if CONF_MANUFACTURER in node_config: 398 | discovered_info[CONF_MANUFACTURER] = device.manufacturer = node_config[CONF_MANUFACTURER] 399 | if CONF_MODEL in node_config: 400 | discovered_info[CONF_MODEL] = device.model = node_config[CONF_MODEL] 401 | # when a model name is available and not the template already applied, 402 | # use it to do custom init 403 | if (device.model and CONF_TEMPLATE not in node_config): 404 | if device.model not in self.custom_devices: 405 | self.custom_devices[device.model] = custom_module = get_custom_device_info(device.model) 406 | else: 407 | custom_module = self.custom_devices[device.model] 408 | _LOGGER.debug('[0x%04x:%s] pre call _custom_endpoint_init: %s', 409 | device.nwk, endpoint_id, 410 | custom_module) 411 | 412 | if custom_module.get('_custom_endpoint_init', None) is not None: 413 | _LOGGER.debug('[0x%04x:%s] call _custom_endpoint_init: %s', 414 | device.nwk, 415 | endpoint_id, 416 | device.model) 417 | custom_module['_custom_endpoint_init'](endpoint, node_config, device.model) 418 | else: 419 | _LOGGER.debug('[0x%04x:%s] no call _custom_endpoint_init: %s', 420 | device.nwk, 421 | endpoint_id, 422 | device.model) 423 | 424 | _LOGGER.debug("[0x%04x:%s] node config for %s: %s", 425 | device.nwk, 426 | endpoint_id, 427 | device_key, 428 | node_config) 429 | 430 | if endpoint.profile_id in zigpy.profiles.PROFILES: 431 | profile = zigpy.profiles.PROFILES[endpoint.profile_id] 432 | if DEVICE_CLASS.get(endpoint.profile_id, {}).get(endpoint.device_type, None): 433 | profile_clusters[0].update(profile.CLUSTERS[endpoint.device_type][0]) 434 | profile_clusters[1].update(profile.CLUSTERS[endpoint.device_type][1]) 435 | profile_info = DEVICE_CLASS[endpoint.profile_id] 436 | component = profile_info[endpoint.device_type] 437 | # Override type (switch,light,sensor, binary_sensor,...) from config 438 | if ha_const.CONF_TYPE in node_config: 439 | component = node_config[ha_const.CONF_TYPE] 440 | if component in COMPONENT_CLUSTERS: 441 | profile_clusters = list(COMPONENT_CLUSTERS[component]) 442 | 443 | # Add allowed In_Clusters from config 444 | if CONF_IN_CLUSTER in node_config: 445 | profile_clusters[0] = set(node_config.get(CONF_IN_CLUSTER)) 446 | # Add allowed Out_Clusters from config 447 | if CONF_OUT_CLUSTER in node_config: 448 | profile_clusters[1] = set(node_config.get(CONF_OUT_CLUSTER)) 449 | 450 | # if reporting is configured in yaml, 451 | # then create cluster if needed and setup reporting 452 | if join and CONF_CONFIG_REPORT in node_config: 453 | for report in node_config.get(CONF_CONFIG_REPORT): 454 | report_cls, report_attr, report_min, report_max, report_change = report[0:5] 455 | mfgCode = None if not report[5:] else report[5] 456 | if report_cls in endpoint.in_clusters: 457 | cluster = endpoint.in_clusters[report_cls] 458 | await req_conf_report( 459 | cluster, 460 | report_attr, 461 | report_min, 462 | report_max, 463 | report_change, 464 | mfgCode=mfgCode) 465 | else: 466 | _LOGGER.debug("[0x%04x:%s] config reports skipped for %s, %s ", 467 | device.nwk, 468 | endpoint_id, 469 | device._ieee, 470 | "no reports configured" if join else "already joined") 471 | 472 | _LOGGER.debug("[0x%04x:%s] 2:profile %s, component: %s cluster:%s in_clusters: %s", 473 | device.nwk, 474 | endpoint_id, 475 | endpoint.profile_id, 476 | component, 477 | profile_clusters, 478 | endpoint.in_clusters) 479 | 480 | in_clusters = set(endpoint.in_clusters.keys()) 481 | out_clusters = set(endpoint.out_clusters.keys()) 482 | sc = {cluster.cluster_id for cluster in SINGLE_CLUSTER_DEVICE_CLASS} 483 | c_intersect= in_clusters & sc 484 | _LOGGER.debug('[0x%04x:%s] Single Cluster: %s', 485 | device.nwk, endpoint_id, c_intersect) 486 | primary_cluster = node_config.get('primary_cluster') 487 | if len(c_intersect)>1: 488 | if primary_cluster: 489 | try: 490 | c_intersect.remove(primary_cluster) 491 | except Excecution: 492 | pass 493 | else: 494 | primary_cluster = sorted(list(c_intersect))[0] 495 | c_intersect.remove(primary_cluster) 496 | else: 497 | c_intersect = set() 498 | 499 | 500 | if component: 501 | # allow all clusters to be used 502 | # only discovered clusters that are in the profile or configuration listed 503 | # in_clusters = [endpoint.in_clusters[c] 504 | # for c in profile_clusters[0] 505 | # if c in endpoint.in_clusters] 506 | # out_clusters = [endpoint.out_clusters[c] 507 | # for c in profile_clusters[1] 508 | # if c in endpoint.out_clusters] 509 | in_clusters -= c_intersect 510 | 511 | _LOGGER.debug("[0x%04x:%s]general entity:%s, component:%s clusters:%s<->%s", 512 | device.nwk, 513 | endpoint_id, 514 | endpoint.profile_id, 515 | component, 516 | in_clusters, 517 | out_clusters, 518 | ) 519 | if in_clusters != [] or out_clusters != []: 520 | 521 | # create discovery info 522 | discovery_info = { 523 | 'endpoint': endpoint, 524 | 'in_clusters': {c: endpoint.in_clusters[c] for c in in_clusters}, 525 | 'out_clusters': {c: endpoint.out_clusters[c] for c in out_clusters}, 526 | 'component': component, 527 | 'device': device, 528 | 'discovery_key': device_key, 529 | 'new_join': join, 530 | 'application': self, 531 | 'model': device.model, 532 | 'manufacturer': device.manufacturer, 533 | } 534 | 535 | self._hass.data[DISCOVERY_KEY][device_key] = discovery_info 536 | """ goto to the specific code for switch, 537 | light sensor or binary_sensor """ 538 | await discovery.async_load_platform( 539 | self._hass, 540 | component, 541 | DOMAIN, 542 | {'discovery_key': device_key}, 543 | self._config, 544 | ) 545 | _LOGGER.debug("[0x%04x:%s] Create general entity:%s", 546 | device.nwk, 547 | endpoint_id, 548 | device._ieee) 549 | 550 | # initialize single clusters 551 | for cluster in c_intersect: 552 | # if ha_const.CONF_TYPE in node_config: 553 | # component = node_config[ha_const.CONF_TYPE] 554 | # else: 555 | component = SINGLE_CLUSTER_DEVICE_CLASS[type(endpoint.in_clusters[cluster])] 556 | _LOGGER.debug("[0x%04x:%s] Create single-cluster entity: %s", 557 | device.nwk, 558 | endpoint_id, 559 | cluster) 560 | cluster_key = '%s-%s' % (device_key, cluster) 561 | # cluster key -> single cluster 562 | discovery_info = { 563 | 'discovery_key': cluster_key, 564 | 'endpoint': endpoint, 565 | 'in_clusters': {cluster: endpoint.in_clusters[cluster]}, 566 | 'out_clusters': {}, 567 | 'new_join': join, 568 | # 'platform': PLATFORM, 569 | 'component': component, 570 | 'application': self 571 | } 572 | discovery_info.update(discovered_info) 573 | 574 | self._hass.data[DISCOVERY_KEY][cluster_key] = discovery_info 575 | 576 | await discovery.async_load_platform( 577 | self._hass, 578 | component, 579 | DOMAIN, 580 | {'discovery_key': cluster_key}, 581 | self._config, 582 | ) 583 | 584 | device._application.listener_event('device_updated', device) 585 | self.controller._state = 'Run' 586 | self.controller.async_schedule_update_ha_state() 587 | _LOGGER.debug("[0x%04x] Exit device init %s", 588 | device.nwk, 589 | device.ieee, 590 | ) 591 | 592 | async def command(self, service_data): 593 | command = service_data.get(ATTR_COMMAND) 594 | entity_id = service_data.get(ATTR_ENTITY_ID) 595 | entity = self._entity_list.get(entity_id, None) 596 | if not entity: 597 | _LOGGER.warn("entity %s unknown", entity_id) 598 | return 599 | if command == 'write_attribute': 600 | try: 601 | # expect cluster, attribute + value as minimal input 602 | cluster = service_data.get('cluster') 603 | attribute = service_data.get('attribute') 604 | value = service_data.get('value') 605 | mgfid = service_data.get('mfgid',) 606 | except KeyError: 607 | pass 608 | 609 | # Todo 610 | # find entity for entity_id 611 | # get endpoint for entity 612 | #write attribute to endpoint 613 | 614 | 615 | 616 | class Entity(RestoreEntity): 617 | 618 | """A base class for ZHA entities.""" 619 | 620 | _domain = None # Must be overridden by subclasses 621 | 622 | def __init__(self, endpoint, in_clusters, out_clusters, manufacturer, 623 | model, **kwargs): 624 | """Init ZHA entity.""" 625 | self._device_state_attributes = {} 626 | self.entity_connect = {} 627 | self.sub_listener = dict() 628 | self._device_class = None 629 | self._groups = None 630 | 631 | ieeetail = ''.join([ 632 | '%02x' % (o, ) for o in endpoint.device.ieee[-4:] 633 | ]) 634 | self.uid = str(endpoint.device._ieee) + "_" + str(endpoint.endpoint_id) 635 | if 'cluster_key' in kwargs: 636 | self.cluster_key = kwargs['cluster_key'] 637 | self.uid += '_' 638 | self.uid += self.cluster_key 639 | if 'application' in kwargs: 640 | self._application = kwargs['application'] 641 | # self._application._entity_list[self.entity_id] = self 642 | # self.platform = kwargs['platform'] 643 | if model in self._application.custom_devices: 644 | self._custom_module = self._application.custom_devices[model] 645 | else: 646 | self._custom_module = {} 647 | if manufacturer is None: 648 | manufacturer = 'unknown' 649 | if model is None: 650 | model = 'unknown' 651 | 652 | self.entity_id = '%s.%s_%s_%s_%s' % ( 653 | self._domain, 654 | slugify(manufacturer), 655 | slugify(model), 656 | ieeetail, 657 | endpoint.endpoint_id, 658 | ) 659 | self.device = endpoint.device 660 | 661 | self._device_state_attributes['friendly_name'] = '%s %s' % ( 662 | manufacturer, 663 | model, 664 | ) 665 | self._device_state_attributes['model'] = model 666 | self._device_state_attributes['manufacturer'] = manufacturer 667 | self._model = model 668 | self._manufacturer = manufacturer 669 | 670 | # else: 671 | # self.entity_id = "%s.zha_%s_%s" % ( 672 | # self._domain, 673 | # ieeetail, 674 | # endpoint.endpoint_id, 675 | # ) 676 | if 'cluster_key' in kwargs: 677 | self.entity_id += '_' 678 | self.entity_id += kwargs['cluster_key'] 679 | self._device_state_attributes['friendly_name'] += '_' 680 | self._device_state_attributes['friendly_name'] += kwargs['cluster_key'] 681 | 682 | self._endpoint = endpoint 683 | self._in_clusters = in_clusters 684 | self._out_clusters = out_clusters 685 | self._state = None 686 | self._device_state_attributes['lqi'] = endpoint.device.lqi 687 | self._device_state_attributes['rssi'] = endpoint.device.rssi 688 | self._device_state_attributes['last seen'] = None 689 | self._device_state_attributes['nwk'] = endpoint.device.nwk 690 | self._device_state_attributes['path'] = 'unknown' 691 | # _LOGGER.debug("dir entity:%s", dir(self)) 692 | if self._custom_module.get('_parse_attribute', None): 693 | self._parse_attribute = self._custom_module['_parse_attribute'] 694 | if self._custom_module.get('_custom_cluster_command', None): 695 | self._custom_cluster_command = self._custom_module['_custom_cluster_command'] 696 | if self._custom_module.get('_custom_endpoint_init', None): 697 | self._custom_endpoint_init = self._custom_module['_custom_endpoint_init'] 698 | 699 | @property 700 | def name(self): 701 | return self.entity_id 702 | 703 | @property 704 | def device_class(self) -> str: 705 | """Return the class of this device, from component DEVICE_CLASSES.""" 706 | return str(self._device_class) 707 | 708 | @property 709 | def unique_id(self): 710 | return self.uid 711 | 712 | def attribute_updated(self, attribute, value): 713 | self._state = value 714 | self.schedule_update_ha_state() 715 | 716 | def zdo_command(self, tsn, command_id, args): 717 | """Handle a ZDO command received on this cluster.""" 718 | _LOGGER.debug("ZDO received: \n entity - \n command_id: %s \n args: %s", 719 | self.entity_id, command_id, args) 720 | 721 | def cluster_command(self, tsn, command_id, args): 722 | """ handle incomming cluster commands.""" 723 | _LOGGER.debug("Cluster received: \n entity - \n command_id: %s \n args: %s", 724 | self.entity_id, command_id, args) 725 | 726 | """dummy function; override from device handler""" 727 | def _custom_cluster_command(self, *args, **kwargs): 728 | return(args, kwargs) 729 | 730 | """dummy function; override from device handler""" 731 | def _parse_attribute(self, *args, **kwargs): 732 | _LOGGER.debug(" dummy parse_attribute called with %s %s", args, kwargs) 733 | return(args, kwargs) 734 | 735 | def _custom_endpoint_init(self, *args, **kwargs): 736 | """dummy function; override from device handler.""" 737 | pass 738 | 739 | @property 740 | def device_state_attributes(self): 741 | """Return device specific state attributes.""" 742 | self._device_state_attributes.update({ 743 | 'lqi': self._endpoint.device.lqi, 744 | 'rssi': self._endpoint.device.rssi, 745 | 'nwk': self._endpoint.device.nwk, 746 | 'path': self._endpoint.device.path, 747 | 'last seen': self._endpoint.device.last_seen, 748 | }) 749 | return self._device_state_attributes 750 | 751 | async def async_added_to_hass(self): 752 | """Call when entity about to be added to hass.""" 753 | await super().async_added_to_hass() 754 | data = self._restore_data = await self.async_get_last_state() 755 | self._application._entity_list[self.entity_id] = self 756 | _LOGGER.debug("entity_list added: %s", self._application._entity_list) 757 | try: 758 | _LOGGER.debug("Restore state for %s:", self.entity_id) 759 | if data is not None and data.state: 760 | if (data.state == '-') or (data.state == ha_const.STATE_UNKNOWN): 761 | self._state = None 762 | elif hasattr(self, 'state_div'): 763 | self._state = float(data.state) * self.state_div 764 | else: 765 | self._state = 1 if data.state == ha_const.STATE_ON else 0 766 | 767 | # self._device_state_attributes.update(data.attributes) 768 | self._device_state_attributes.pop('assumed_state', None) 769 | self.device_state_attributes.pop('brightness', None) 770 | if not self._groups: 771 | self._groups = data.attributes.get("Group_id", list()) 772 | for group in self._groups: 773 | self._endpoint._device._application.listener_event( 774 | 'subscribe_group', 775 | group) 776 | self._device_state_attributes['Group_id'] = self._groups 777 | except Exception as e: 778 | _LOGGER.exception('Restore failed for %s: %s', self.entity_id, e) 779 | 780 | @property 781 | def assumed_state(self): 782 | """Return True if unable to access real state of the entity.""" 783 | return False 784 | 785 | async def async_will_remove_from_hass(self) -> None: 786 | """ Run when entity will be removedd from hass.""" 787 | await super().async_will_remove_from_hass() 788 | try: 789 | self._application._entity_list.pop(self.entity_id) 790 | except KeyError: 791 | pass 792 | 793 | @property 794 | def device_info(self): 795 | return{ 796 | 'connections': {(CONNECTION_ZIGBEE, self._endpoint.device._ieee)}, 797 | 'identifiers': {(DOMAIN, self._endpoint.device._ieee)}, 798 | 'model': self._model, 799 | 'manufacturer': self._manufacturer, 800 | } 801 | 802 | async def async_update(self): 803 | _LOGGER.debug('[%s] Entity async_update called', 804 | self.entity_id, 805 | ) 806 | 807 | 808 | class MEntity(Entity): 809 | 810 | """ A dummy entity for multicasts. """ 811 | def __init__(self, **dicovery_info): 812 | 813 | """Init ZHA entity.""" 814 | in_clusters = entity_.out_clusters 815 | out_clusters = () 816 | self._supported_features = 0b00111111 817 | self._available = True 818 | self._assumed = False 819 | self._device_class = None 820 | self.uid = 'zha_group_sender' 821 | self.manufacturer = 'YODA' 822 | self.model = 'zha_group_sender' 823 | if 'application' in kwargs: 824 | self._application = kwargs['application'] 825 | self._hidden= True 826 | 827 | self.entity_id = '%s.MC_%s' % ( 828 | self._domain, 829 | slugify(group_id), 830 | ) 831 | # self.device = endpoint.device 832 | 833 | self._device_state_attributes['friendly_name'] = '%s %s' % ( 834 | self.manufacturer, 835 | self.model, 836 | ) 837 | self._device_state_attributes['model'] = self.model 838 | self._device_state_attributes['manufacturer'] = self.manufacturer 839 | 840 | 841 | self._endpoint = endpoint 842 | self._in_clusters = in_clusters 843 | self._out_clusters = out_clusters 844 | self._state = True 845 | 846 | async def _discover_endpoint_info(endpoint): 847 | import string 848 | """Find some basic information about an endpoint.""" 849 | extra_info = { 850 | 'manufacturer': None, 851 | 'model': None, 852 | } 853 | 854 | async def read(attributes): 855 | """Read attributes and update extra_info convenience function.""" 856 | result, _ = await endpoint.in_clusters[0].read_attributes( 857 | attributes, 858 | allow_cache=False, 859 | ) 860 | extra_info.update(result) 861 | _LOGGER.debug("read attribute: %s", result) 862 | 863 | # try: 864 | # await read(['model','manufacturer']) 865 | # except: 866 | # _LOGGER.debug("read attribute failed: mode/manufacturer") 867 | try: 868 | await read(['model']) 869 | except Exception as e: 870 | _LOGGER.debug("single read attribute failed: model, %s", e) 871 | try: 872 | await read(['manufacturer']) 873 | except Exception as e: 874 | _LOGGER.debug("single read attribute failed: manufacturer, %s", e) 875 | for key, value in extra_info.items(): 876 | _LOGGER.debug("%s: type(%s) %s", key, type(value), value) 877 | if isinstance(value, bytes): 878 | try: 879 | value = value.decode('ascii').strip() 880 | extra_info[key] = ''.join([x for x in value if x in string.printable]) 881 | _LOGGER.debug("%s: type(%s) %s", key, type(extra_info[key]), extra_info[key]) 882 | except UnicodeDecodeError as e: 883 | # Unsure what the best behaviour here is. Unset the key? 884 | _LOGGER.debug("unicode decode error, %s", e) 885 | _LOGGER.debug("discover_endpoint_info:%s", extra_info) 886 | return extra_info 887 | 888 | 889 | def get_discovery_info(hass, discovery_info): 890 | """Get the full discovery info for a device. 891 | 892 | Some of the info that needs to be passed to platforms is not JSON 893 | serializable, so it cannot be put in the discovery_info dictionary. This 894 | component places that info we need to pass to the platform in hass.data, 895 | and this function is a helper for platforms to retrieve the complete 896 | discovery info. 897 | 898 | """ 899 | if discovery_info is None: 900 | return 901 | discovery_key = discovery_info.get('discovery_key', None) 902 | all_discovery_info = hass.data.get(DISCOVERY_KEY, {}) 903 | discovery_info = all_discovery_info.get(discovery_key, None) 904 | return discovery_info 905 | 906 | 907 | async def attribute_read(endpoint, cluster, attributes): 908 | """Read attributes and update extra_info convenience fcunction.""" 909 | result = await endpoint.in_clusters[cluster].read_attributes( 910 | attributes, 911 | allow_cache=True, 912 | ) 913 | return result 914 | 915 | 916 | async def get_battery(endpoint): 917 | if 1 not in endpoint.in_clusters: 918 | return 0xff 919 | battery = await attribute_read(endpoint, 0x0001, ['battery_voltage']) 920 | return battery[0] 921 | 922 | 923 | async def discover_cluster_values(endpoint, cluster): 924 | attrids = [0, ] 925 | _LOGGER.debug("discover %s-%s for %s", 926 | endpoint._device.ieee, 927 | endpoint._endpoint_id, 928 | cluster.cluster_id) 929 | try: 930 | v = await cluster.discover_attributes(0, 32) 931 | except: 932 | pass 933 | _LOGGER.debug("discover %s for %s: %s", endpoint._endpoint_id, cluster.cluster_id, v[0]) 934 | if isinstance(v[0], int): 935 | attrids = [0, 1, 2, 3, 4, 5, 6, 7, 8, 16, 17, 18] 936 | else: 937 | for item in v[0]: 938 | attrids.append(item.attrid) 939 | _LOGGER.debug("discover_cluster_attributes: query %s:", attrids) 940 | try: 941 | v = await cluster.read_attributes(attrids, allow_cache=True) 942 | _LOGGER.debug("attributes/values for cluster:%s", v[0]) 943 | except: 944 | return({}) 945 | return(v[0]) 946 | 947 | def get_custom_device_info(_model): 948 | custom_info = dict() 949 | 950 | try: 951 | dev_func = str(_model).lower().replace(".", "_").replace(" ", "_") 952 | device_module = import_module("custom_components.device." + dev_func) 953 | _LOGGER.debug("Import DH %s success", _model) 954 | except ImportError as e: 955 | _LOGGER.debug("Import DH %s failed: %s", _model, e.args) 956 | return {} 957 | custom_info['module'] = device_module 958 | custom_info['_custom_endpoint_init'] = getattr( 959 | device_module, 960 | '_custom_endpoint_init', 961 | None) 962 | custom_info['_custom_cluster_command'] = getattr( 963 | device_module, 964 | '_custom_cluster_command', 965 | None) 966 | custom_info['_parse_attribute'] = getattr( 967 | device_module, 968 | '_parse_attribute', 969 | None) 970 | custom_info['custom_parameters'] = getattr( 971 | device_module, 972 | 'custom_parmeters', 973 | None) 974 | _LOGGER.debug('custom_info for %s: %s', _model, custom_info) 975 | return custom_info 976 | 977 | 978 | def call_func(_model, function, *args): 979 | try: 980 | dev_func = _model.lower().replace(".", "_").replace(" ", "_") 981 | 982 | call_function = getattr( 983 | import_module("custom_components.device." + dev_func), 984 | function 985 | ) 986 | return call_function(args) 987 | except ImportError as e: 988 | _LOGGER.debug("Import DH %s failed: %s", function, e.args) 989 | except Exception as e: 990 | _LOGGER.info("Excecution of DH %s failed: %s", dev_func, e.args) 991 | 992 | 993 | async def req_conf_report(report_cls, report_attr, report_min, report_max, report_change, mfgCode=None): 994 | from zigpy.zcl.foundation import Status 995 | 996 | endpoint = report_cls._endpoint 997 | try: 998 | v = await report_cls.bind() 999 | if v[0] > 0: 1000 | _LOGGER.debug("[0x%04x:%s:0x%04x]: bind failed: %s", 1001 | endpoint._device.nwk, 1002 | endpoint.endpoint_id, 1003 | report_cls.cluster_id, 1004 | Status(v[0]).name) 1005 | except Exception as e: 1006 | _LOGGER.debug("[0x%04x:%s:0x%04x]: : bind exceptional failed %s", 1007 | endpoint._device.nwk, 1008 | endpoint.endpoint_id, 1009 | report_cls.cluster_id, 1010 | e) 1011 | try: 1012 | v = await report_cls.configure_reporting( 1013 | report_attr, int(report_min), 1014 | int(report_max), report_change, manufacturer=mfgCode) 1015 | _LOGGER.debug("[0x%04x:%s:0x%04x] set config report status: %s", 1016 | endpoint._device.nwk, 1017 | endpoint._endpoint_id, 1018 | report_cls.cluster_id, 1019 | v) 1020 | except Exception as e: 1021 | _LOGGER.error("[0x%04x:%s:0x%04x] set config report exeptional failed: %s", 1022 | endpoint._device.nwk, 1023 | endpoint.endpoint_id, 1024 | report_cls.cluster_id, 1025 | e) 1026 | 1027 | class zha_state(entity.Entity): 1028 | 1029 | 1030 | def __init__(self, hass, stack, application, name, state='Init'): 1031 | self._device_state_attributes = {} 1032 | self._device_state_attributes['friendly_name'] = 'Controller' 1033 | self.hass = hass 1034 | self._state = state 1035 | self.entity_id = DOMAIN + '.' + name 1036 | self.DOMAIN = DOMAIN 1037 | self.stack = stack 1038 | self.application = application 1039 | 1040 | @property 1041 | def state(self): 1042 | """Return the state of the sensor.""" 1043 | return self._state 1044 | 1045 | @property 1046 | def device_state_attributes(self): 1047 | """Return device specific state attributes.""" 1048 | return self._device_state_attributes 1049 | 1050 | @property 1051 | def icon(self): 1052 | if self._state == "Failed": 1053 | return 'mdi:skull-crossbones' 1054 | else: 1055 | return 'mdi:emoticon-happy' 1056 | 1057 | async def async_update(self): 1058 | result = await self.stack._command('neighborCount', []) 1059 | self._device_state_attributes['neighborCount'] = result[0] 1060 | entity_store = get_entity_store(self.hass) 1061 | self._device_state_attributes['no_entities'] = len(entity_store) 1062 | self._device_state_attributes['no_devices'] = len(self.application.devices) 1063 | # result = await self.stack._command('getValue', 3) 1064 | # _LOGGER.debug("buffer: %s", result[1]) 1065 | # buffer = t.uint8_t(result[1]) 1066 | # self._device_state_attributes['FreeBuffers'] = buffer 1067 | # result = await self.stack._command('getSourceRouteTableFilledSize', []) 1068 | # self._device_state_attributes['getSourceRouteTableFilledSize'] = result[0] 1069 | # neighbors = await self.application.read_neighbor_table() 1070 | # self._device_state_attributes['neighbors'] = neighbors 1071 | # await self.application.update_topology() 1072 | stats = self.application.stats() 1073 | for key, value in stats.items(): 1074 | self._device_state_attributes[key] = value 1075 | status = self.application.status() 1076 | self._device_state_attributes['status'] = status 1077 | if (sum(status[0]) + sum(status[1]) > 0): 1078 | self._state = "Failed" 1079 | elif self._state == "Failed": 1080 | self._state = "Run" 1081 | self._device_state_attributes['Group_id'] = self.application._groups 1082 | -------------------------------------------------------------------------------- /custom_components/zha_new/binary_sensor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Binary sensors on Zigbee Home Automation networks. 3 | 4 | For more details on this platform, please refer to the documentation 5 | 6 | at https://home-assistant.io/components/binary_sensor.zha/ 7 | 8 | """ 9 | import asyncio 10 | import logging 11 | import datetime 12 | import homeassistant.util.dt as dt_util 13 | from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice 14 | import custom_components.zha_new as zha_new 15 | import custom_components.zha_new.helpers as helpers 16 | from homeassistant.helpers.event import async_track_point_in_time 17 | from zigpy.zdo.types import Status 18 | from zigpy.zcl.clusters.general import LevelControl, OnOff, Scenes 19 | from zigpy.zcl.clusters.lightlink import LightLink 20 | from zigpy.zcl.clusters.general import Basic, PowerConfiguration 21 | from zigpy.zcl.clusters.security import IasZone 22 | from zigpy.zcl.clusters.measurement import OccupancySensing 23 | from zigpy.zcl.clusters.measurement import TemperatureMeasurement 24 | from .const import DOMAIN as PLATFORM 25 | _LOGGER = logging.getLogger(__name__) 26 | from custom_components.zha_new.cluster_handler import ( 27 | Cluster_Server, 28 | Server_OnOff, 29 | Server_Scenes, 30 | Server_Basic, 31 | Server_LevelControl, 32 | Server_IasZone, 33 | Server_OccupancySensing, 34 | Server_TemperatureMeasurement, 35 | Server_PowerConfiguration, 36 | Server_LightLink, 37 | ) 38 | # ZigBee Cluster Library Zone Type to Home Assistant device class 39 | CLASS_MAPPING = { 40 | 0x000d: 'motion', 41 | 0x0015: 'opening', 42 | 0x0028: 'smoke', 43 | 0x002a: 'moisture', 44 | 0x002b: 'gas', 45 | 0x002d: 'vibration', 46 | } 47 | 48 | 49 | def setup_platform( 50 | hass, config, async_add_devices, discovery_info=None): 51 | _LOGGER.debug("disocery info setup_platform: %s", discovery_info) 52 | 53 | return True 54 | 55 | 56 | async def async_setup_platform( 57 | hass, config, async_add_devices, discovery_info=None): 58 | """Set up the Zigbee Home Automation binary sensors.""" 59 | discovery_info = zha_new.get_discovery_info(hass, discovery_info) 60 | 61 | if discovery_info is None: 62 | return 63 | 64 | in_clusters = discovery_info['in_clusters'] 65 | endpoint = discovery_info['endpoint'] 66 | application = discovery_info['application'] 67 | device_class = None 68 | # groups = None 69 | 70 | if discovery_info['new_join']: 71 | # if 0x1000 in endpoint.in_clusters: 72 | # try: 73 | # groups = await helpers.cluster_commisioning_groups( 74 | # endpoint.in_clusters[0x1000]) 75 | # except Exception as e: 76 | # _LOGGER.debug( 77 | # "catched exception in commissioning group_id %s", e) 78 | 79 | """ create ias cluster if it not already exists""" 80 | if IasZone.cluster_id not in in_clusters: 81 | cluster = endpoint.add_input_cluster(IasZone.cluster_id) 82 | in_clusters[IasZone.cluster_id] = cluster 83 | endpoint.in_clusters[IasZone.cluster_id] = cluster 84 | else: 85 | cluster = in_clusters[IasZone.cluster_id] 86 | await cluster.bind() 87 | 88 | try: 89 | ieee = cluster.endpoint.device.application.ieee 90 | result = await cluster.write_attributes({'cie_addr': ieee}) 91 | _LOGGER.debug("write cie:%s", result) 92 | except Exception: 93 | _LOGGER.debug("bind/write cie failed") 94 | else: 95 | if not result: 96 | try: 97 | await cluster.enroll_response(0, 0) 98 | except Exception: 99 | _LOGGER.debug("send enroll_command failed") 100 | 101 | try: 102 | _LOGGER.debug("try zone read") 103 | zone_type = await cluster['zone_type'] 104 | _LOGGER.debug("done zone read") 105 | device_class = CLASS_MAPPING.get(zone_type, None) 106 | except Exception: # pylint: disable=broad-except 107 | _LOGGER.debug("zone read failed") 108 | 109 | # discovery_info['groups'] = groups 110 | entity = await _make_sensor(device_class, discovery_info) 111 | 112 | # initialize/discover clusters 113 | if discovery_info['new_join']: 114 | for CH in entity.sub_listener.values(): 115 | await CH.join_prepare() 116 | 117 | e_registry = await hass.helpers.entity_registry.async_get_registry() 118 | reg_dev_id = e_registry.async_get_or_create( 119 | DOMAIN, PLATFORM, entity.uid, 120 | suggested_object_id=entity.entity_id, 121 | device_id=str(entity.device._ieee) 122 | ) 123 | if entity.entity_id != reg_dev_id.entity_id and 'unknown' in reg_dev_id.entity_id: 124 | _LOGGER.debug("entity different name,change it: %s", reg_dev_id) 125 | e_registry.async_update_entity(reg_dev_id.entity_id, 126 | new_entity_id=entity.entity_id) 127 | if reg_dev_id.entity_id in application._entity_list: 128 | _LOGGER.debug("entity exist,remove it: %s", reg_dev_id) 129 | await application._entity_list.get(reg_dev_id.entity_id).async_remove() 130 | async_add_devices([entity]) 131 | 132 | _LOGGER.debug("set Entity object: %s-%s ", type(entity), entity.unique_id) 133 | entity_store = zha_new.get_entity_store(hass) 134 | if endpoint.device._ieee not in entity_store: 135 | entity_store[endpoint.device._ieee] = [] 136 | entity_store[endpoint.device._ieee].append(entity) 137 | 138 | endpoint._device._application.listener_event('device_updated', 139 | endpoint._device) 140 | _LOGGER.debug("Return binary_sensor init-cluster %s", endpoint.in_clusters) 141 | 142 | 143 | async def _make_sensor(device_class, discovery_info): 144 | """Create ZHA sensors factory.""" 145 | 146 | in_clusters = discovery_info['in_clusters'] 147 | out_clusters = discovery_info['out_clusters'] 148 | endpoint = discovery_info['endpoint'] 149 | if endpoint.device_type in ( 150 | 0x0800, 151 | 0x0810, 152 | 0x0820, 153 | 0x0830, 154 | 0x0000, 155 | 0x0001, 156 | 0x0006, 157 | ): 158 | sensor = RemoteSensor('remote', **discovery_info) 159 | elif device_class == 'moisture': 160 | sensor = MoistureSensor('moisture', **discovery_info) 161 | elif device_class == 'motion': 162 | sensor = OccupancySensor('motion', **discovery_info) 163 | elif (OnOff.cluster_id in in_clusters 164 | or OnOff.cluster_id in out_clusters): 165 | sensor = OnOffSensor('opening', 166 | **discovery_info, 167 | cluster_key=OnOff.ep_attribute) 168 | elif (OccupancySensing.cluster_id in in_clusters 169 | or OccupancySensing.cluster_id in out_clusters): 170 | sensor = OccupancySensor('motion', 171 | **discovery_info, 172 | cluster_key=OccupancySensing.ep_attribute) 173 | else: 174 | sensor = BinarySensor(device_class, **discovery_info) 175 | 176 | if discovery_info['new_join']: 177 | for cluster in in_clusters.values(): 178 | try: 179 | v = await cluster.bind() 180 | except Exception: 181 | v = [Status.TIMEOUT] 182 | if v[0]: 183 | _LOGGER.error("[0x%04x:%s] bind input-cluster failed %s", 184 | endpoint._device.nwk, endpoint.endpoint_id, 185 | Status(v[0]).name 186 | ) 187 | _LOGGER.debug("[0x%04x:%s] bind input-cluster %s: %s", 188 | endpoint._device.nwk, 189 | endpoint.endpoint_id, 190 | cluster.cluster_id, 191 | v) 192 | 193 | _LOGGER.debug("[0x%04x:%s] exit make binary-sensor ", 194 | endpoint._device.nwk, 195 | endpoint.endpoint_id) 196 | return sensor 197 | 198 | ######################################################### 199 | # Binary Sensor Classes ######################################## 200 | 201 | 202 | class BinarySensor(zha_new.Entity, BinarySensorDevice): 203 | 204 | """THe ZHA Binary Sensor.""" 205 | 206 | _domain = DOMAIN 207 | value_attribute = 0 208 | 209 | def __init__(self, device_class, **kwargs): 210 | """Initialize the ZHA binary sensor.""" 211 | super().__init__(**kwargs) 212 | self._device_class = device_class 213 | # self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id] 214 | endpoint = kwargs['endpoint'] 215 | self._groups = kwargs.get('groups', None) 216 | in_clusters = kwargs['in_clusters'] 217 | out_clusters = kwargs['out_clusters'] 218 | clusters = list(out_clusters.items()) + list(in_clusters.items()) 219 | _LOGGER.debug("[0x%04x:%s] initialize cluster listeners: -%s- ", 220 | endpoint._device.nwk, 221 | endpoint.endpoint_id, 222 | clusters) 223 | 224 | for (_, cluster) in clusters: 225 | if LevelControl.cluster_id == cluster.cluster_id: 226 | self.sub_listener[cluster.cluster_id] = Server_LevelControl( 227 | self, cluster, 'Level') 228 | elif OnOff.cluster_id == cluster.cluster_id: 229 | self.sub_listener[cluster.cluster_id] = Server_OnOff( 230 | self, cluster, 'OnOff') 231 | elif Scenes.cluster_id == cluster.cluster_id: 232 | self.sub_listener[cluster.cluster_id] = Server_Scenes( 233 | self, cluster, "Scenes") 234 | elif IasZone.cluster_id == cluster.cluster_id: 235 | self.sub_listener[cluster.cluster_id] = Server_IasZone( 236 | self, cluster, "IasZone") 237 | elif Basic.cluster_id == cluster.cluster_id: 238 | self.sub_listener[cluster.cluster_id] = Server_Basic( 239 | self, cluster, "Basic") 240 | elif OccupancySensing.cluster_id == cluster.cluster_id: 241 | self.sub_listener[cluster.cluster_id] = Server_OccupancySensing( 242 | self, cluster, "OccupancySensing") 243 | elif TemperatureMeasurement.cluster_id == cluster.cluster_id: 244 | self.sub_listener[cluster.cluster_id] = Server_TemperatureMeasurement( 245 | self, cluster, "TemperatureMeasurement") 246 | elif PowerConfiguration.cluster_id == cluster.cluster_id: 247 | self.sub_listener[cluster.cluster_id] = Server_PowerConfiguration( 248 | self, cluster, "PowerConfiguration") 249 | elif LightLink.cluster_id == cluster.cluster_id: 250 | self.sub_listener[cluster.cluster_id] = Server_LightLink( 251 | self, cluster, "LightLink") 252 | 253 | 254 | else: 255 | self.sub_listener[cluster.cluster_id] = Cluster_Server( 256 | self, cluster, cluster.cluster_id) 257 | endpoint._device.zdo.add_listener(self) 258 | # asyncio.ensure_future(helpers.full_discovery(self._endpoint, timeout=10)) 259 | 260 | @property 261 | def is_on(self) -> bool: 262 | """Return True if entity is on.""" 263 | return bool(self._state) 264 | 265 | @property 266 | def should_poll(self) -> bool: 267 | """Return True if entity has to be polled for state. 268 | False if entity pushes its state to HA. 269 | """ 270 | return False 271 | 272 | @property 273 | def device_class(self): 274 | """Return the class of this device, from component DEVICE_CLASSES.""" 275 | return self._device_class 276 | 277 | # def cluster_command(self, tsn, command_id, args): 278 | # """Handle commands received to this cluster.""" 279 | # if command_id == 0: 280 | # self._state = args[0] & 3 281 | # _LOGGER.debug("Updated alarm state: %s", self._state) 282 | # self.schedule_update_ha_state() 283 | # elif command_id == 1: 284 | # _LOGGER.debug("Enroll requested") 285 | # self.hass.add_job(self._ias_zone_cluster.enroll_response(0, 0)) 286 | 287 | def attribute_updated(self, attribute, value): 288 | _LOGGER.debug("Attribute received on entity: %s %s", attribute, value) 289 | (attribute, value) = self._parse_attribute( 290 | self, 291 | attribute, 292 | value, 293 | self._model, 294 | cluster_id=None) 295 | if attribute == self.value_attribute: 296 | self._state = value 297 | self.schedule_update_ha_state() 298 | 299 | async def device_announce(self, *args, **kwargs): 300 | _LOGGER.debug( 301 | "0x%04x device announce for BINARY_SENSOR received", 302 | self._endpoint._device.nwk 303 | ) 304 | # asyncio.ensure_future(helpers.full_discovery(self._endpoint, timeout=14)) 305 | if 0x1000 in self._endpoint.in_clusters: 306 | try: 307 | groups = await helpers.cluster_commisioning_groups( 308 | self._endpoint.in_clusters[0x1000], 309 | timeout=10 310 | ) 311 | except Exception as e: 312 | _LOGGER.debug("catched exception in commissioning group_id %s", e) 313 | for group in groups: 314 | self._endpoint._device._application.listener_event( 315 | 'subscribe_group', 316 | group) 317 | 318 | 319 | class OccupancySensor(BinarySensor): 320 | 321 | """ ZHA Occupancy Sensor.""" 322 | 323 | value_attribute = 0 324 | re_arm_sec = 20 325 | invalidate_after = None 326 | _state = 0 327 | 328 | def attribute_updated(self, attribute, value): 329 | """ handle trigger events from motion sensor. 330 | clear state after re_arm_sec seconds.""" 331 | _LOGGER.debug("Attribute received: %s %s", attribute, value) 332 | (attribute, value) = self._parse_attribute( 333 | self, 334 | attribute, 335 | value, 336 | self._model, 337 | cluster_id=None, 338 | ) 339 | 340 | @asyncio.coroutine 341 | def _async_clear_state(entity): 342 | _LOGGER.debug("async_clear_state") 343 | if (entity.invalidate_after is None 344 | or entity.invalidate_after < dt_util.utcnow()): 345 | entity._state = bool(0) 346 | entity.schedule_update_ha_state() 347 | 348 | if attribute == self.value_attribute: 349 | self._state = value 350 | self.invalidate_after = dt_util.utcnow() + datetime.timedelta( 351 | seconds=self.re_arm_sec) 352 | self._device_state_attributes['last detection'] \ 353 | = self.invalidate_after 354 | async_track_point_in_time( 355 | self.hass, _async_clear_state(self), 356 | self.invalidate_after) 357 | self.schedule_update_ha_state() 358 | 359 | 360 | class OnOffSensor(BinarySensor): 361 | 362 | """ ZHA On Off Sensor.""" 363 | 364 | value_attribute = 0 365 | cluster_default = 0x0006 366 | 367 | 368 | class MoistureSensor(BinarySensor): 369 | 370 | """ ZHA Moisture Sensor.""" 371 | 372 | value_attribute = 0 373 | 374 | 375 | class RemoteSensor(BinarySensor): 376 | 377 | """Remote controllers.""" 378 | 379 | def __init__(self, device_class, **kwargs): 380 | super().__init__(device_class, **kwargs) 381 | self._brightness = 0 382 | self._supported_features = 0 383 | 384 | def cluster_command(self, tsn, command_id, args): 385 | update_attrib = {} 386 | update_attrib['last seen'] = dt_util.now() 387 | self._entity._device_state_attributes.update({ 388 | 'last seen': dt_util.now(), 389 | self._identifier: self.value, 390 | 'channels': list(c.identifier for c in self.sub_listener_out.items()) 391 | }) 392 | self._entity.schedule_update_ha_state() 393 | self.schedule_update_ha_state() 394 | -------------------------------------------------------------------------------- /custom_components/zha_new/cluster_handler.py: -------------------------------------------------------------------------------- 1 | 2 | import logging 3 | import homeassistant.util.dt as dt_util 4 | import asyncio 5 | import zigpy.types as t 6 | import datetime 7 | from homeassistant.helpers.event import async_track_point_in_time 8 | from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement 9 | from zigpy.zcl.clusters.general import LevelControl, OnOff, Scenes 10 | from zigpy.zcl.clusters.lightlink import LightLink 11 | from zigpy.zcl.clusters.general import Basic, PowerConfiguration, Alarms 12 | from zigpy.zcl.clusters.security import IasZone 13 | from zigpy.zcl.clusters.measurement import OccupancySensing 14 | from zigpy.zcl.clusters.measurement import TemperatureMeasurement 15 | from .helpers import req_conf_report, safe_read, cluster_discover_attributes 16 | from .const import BatteryType 17 | _LOGGER = logging.getLogger(__name__) 18 | 19 | 20 | class Cluster_Server(object): 21 | def __init__(self, entity, cluster, identifier): 22 | self._cluster = cluster 23 | self._entity = entity 24 | self._identifier = identifier 25 | self._value = int(0) 26 | self.value = int(0) 27 | self._prev_tsn = int() 28 | cluster.add_listener(self) 29 | # overwrite function with device specific function 30 | if self._entity._custom_module.get('_parse_attribute', None): 31 | self._parse_attribute = self._entity._custom_module['_parse_attribute'] 32 | 33 | def _parse_attribute(self, entity, attr, value, *args, **kwargs): 34 | _LOGGER.debug("called _parse attribute for &s-%s", self.entity, self._identifier) 35 | return (attr, value) 36 | 37 | def cluster_command(self, tsn, command_id, args): 38 | _LOGGER.debug('cluster command received:[0x%04x:%s] %s', 39 | self._cluster.cluster_id, 40 | command_id, 41 | args 42 | ) 43 | 44 | def attribute_updated(self, attribute, value): 45 | _LOGGER.debug('Attribute report received on cluster [0x%04x:%s:]=%s', 46 | self._cluster.cluster_id, 47 | attribute, 48 | value 49 | ) 50 | 51 | (attribute, value) = self._parse_attribute( 52 | self._entity, 53 | attribute, 54 | value, 55 | self._entity._model, 56 | cluster_id=self._cluster.cluster_id) 57 | try: 58 | if attribute == self._entity.value_attribute: 59 | self._entity._state = value 60 | except Exception: 61 | pass 62 | self._entity.schedule_update_ha_state() 63 | 64 | async def join_prepare(self, **kwargs): 65 | return 66 | 67 | async def async_update(self): 68 | _LOGGER.debug('[%s:0x%04x] cluster async_update called', 69 | self._entity.entity_id, 70 | self._cluster.cluster_id, 71 | ) 72 | 73 | 74 | 75 | class Server_Basic(Cluster_Server): 76 | def cluster_command(self, tsn, command_id, args): 77 | from zigpy.zcl.clusters.general import Basic 78 | if tsn == self._prev_tsn: 79 | return 80 | self._prev_tsn = tsn 81 | command = Basic.server_commands.get(command_id, ('unknown', )) 82 | event_data = { 83 | 'entity_id': self._entity.entity_id, 84 | 'channel': self._identifier, 85 | 'command': command 86 | } 87 | self._entity.hass.bus.fire('click', event_data) 88 | _LOGGER.debug('click event [tsn:%s] %s', tsn, event_data) 89 | self._entity._device_state_attributes.update({ 90 | 'last seen': dt_util.now(), 91 | 'last command': command 92 | }) 93 | self._entity.schedule_update_ha_state() 94 | 95 | 96 | class Server_IasZone(Cluster_Server): 97 | 98 | def __init__(self, entity, cluster, identifier): 99 | self._ZoneStatus = t.bitmap16(0) 100 | super().__init__(entity, cluster, identifier) 101 | self.Status_Names = { 102 | 0: 'ALARM1', 103 | 1: 'ALARM2', 104 | 2: 'TAMPER', 105 | 3: 'BATTERY', 106 | 4: 'SUPERVISION_REPORTS', 107 | 5: 'RESTORE_REPORTS', 108 | 6: 'TROUBLE', 109 | 7: 'AC_MAINS', 110 | 8: 'TEST', 111 | 9: 'BATTERY_DEF', 112 | } 113 | 114 | def cluster_command(self, tsn, command_id, args): 115 | if tsn == self._prev_tsn: 116 | return 117 | self._prev_tsn = tsn 118 | if command_id == 0: 119 | attributes = { 120 | 'last seen': dt_util.now(), 121 | } 122 | zone_change = self._ZoneStatus ^ args[0] 123 | self._ZoneStatus = args[0] 124 | for i in range(len(self.Status_Names)): 125 | attributes[self.Status_Names[i]] = (self._ZoneStatus >> i) & 1 126 | if (zone_change >> i) & 1: 127 | event_data = { 128 | 'entity_id': self._entity.entity_id, 129 | 'channel': self._identifier, 130 | 'command': self.Status_Names[i], 131 | 'data': (self._ZoneStatus >> i) & 1, 132 | } 133 | self._entity.hass.bus.fire('alarm', event_data) 134 | _LOGGER.debug('alarm event [tsn:%s] %s', tsn, event_data) 135 | attributes['last detection'] = dt_util.now() 136 | self._entity._device_state_attributes.update(attributes) 137 | self._entity._state = args[0] & 3 138 | self._entity.schedule_update_ha_state() 139 | elif command_id == 1: 140 | _LOGGER.debug("Enroll requested") 141 | self._entity.hass.add_job(self._cluster.enroll_response(0, 0)) 142 | 143 | 144 | class Server_LevelControl(Cluster_Server): 145 | def __init__(self, entity, cluster, identifier): 146 | 147 | self.start_time = None 148 | self.step = int() 149 | self.on_off = None 150 | super().__init__(entity, cluster, identifier) 151 | 152 | def cluster_command(self, tsn, command_id, args): 153 | from zigpy.zcl.clusters.general import LevelControl 154 | if tsn == self._prev_tsn: 155 | return 156 | self._prev_tsn = tsn 157 | command = LevelControl.server_commands.get(command_id, ('unknown', ))[0] 158 | event_data = { 159 | 'entity_id': self._entity.entity_id, 160 | 'channel': self._identifier, 161 | 'command': command 162 | } 163 | if command in ('move_with_on_off', 'step_with_on_off'): 164 | self.on_off = True 165 | 166 | if command in ('step', 'step_with_on_off'): 167 | if args[0] == 0: 168 | event_data['up_down'] = 1 169 | elif args[0] == 1: 170 | event_data['up_down'] = -1 171 | if args[1] == 0: 172 | self._value = 254 173 | self._entity._state = 1 174 | event_data['step'] = args[1] 175 | self._value += event_data['up_down'] * event_data['step'] 176 | if self._value <= 0: 177 | if self.on_off: 178 | self._entity._state = 0 179 | self.value = 1 180 | self._value = 1 181 | elif self._value > 255: 182 | self._value = 254 183 | self.value = 254 184 | else: 185 | self.value = int(self._value) 186 | if self.on_off: 187 | self._entity._state = 1 188 | # elif command == 'move_to_level_with_on_off': 189 | # self.value = self._value 190 | elif command in ('move_with_on_off', 'move'): 191 | if args[0] == 0: 192 | event_data['up_down'] = 1 193 | elif args[0] == 1: 194 | event_data['up_down'] = -1 195 | self.step = args[1] * event_data['up_down'] 196 | event_data['step'] = args[1] 197 | if self.start_time is None: 198 | self.start_time = dt_util.utcnow().timestamp() 199 | 200 | elif command == 'stop': 201 | if self.start_time is not None: 202 | delta_time = dt_util.utcnow().timestamp() - self.start_time 203 | _LOGGER.debug('Delta: %s move: %s', delta_time, delta_time * self.step) 204 | self._value += int(delta_time * self.step) 205 | self.start_time = None 206 | if self._value <= 1: 207 | if self.on_off: 208 | self._entity._state = 0 209 | self.value = 1 210 | self._value = 1 211 | elif self._value >= 254: 212 | 213 | self._value = 254 214 | self.value = 254 215 | else: 216 | self.value = int(self._value) 217 | if self.on_off: 218 | self._entity._state = 1 219 | 220 | self._entity.hass.bus.fire('click', event_data) 221 | _LOGGER.debug('click event [tsn:%s] %s', tsn, event_data) 222 | self._entity._device_state_attributes.update({ 223 | 'last seen': dt_util.now(), 224 | self._identifier: self.value, 225 | 'last command': command 226 | }) 227 | self._entity.schedule_update_ha_state() 228 | 229 | 230 | class Server_OnOff(Cluster_Server): 231 | def cluster_command(self, tsn, command_id, args): 232 | 233 | if tsn == self._prev_tsn: 234 | return 235 | self._prev_tsn = tsn 236 | command = OnOff.server_commands.get(command_id, ('unknown', ))[0] 237 | event_data = { 238 | 'entity_id': self._entity.entity_id, 239 | 'channel': self._identifier, 240 | 'command': command 241 | } 242 | if command == 'on': 243 | self._entity._state = 1 244 | elif command == 'off': 245 | self._entity._state = 0 246 | elif command == 'toggle': 247 | self._entity._state = self._entity._state ^ 1 248 | self._entity.hass.bus.fire('click', event_data) 249 | _LOGGER.debug('click event [tsn:%s] %s', tsn, event_data) 250 | self._entity._device_state_attributes.update({ 251 | 'last seen': dt_util.now(), 252 | self._identifier: self._value, 253 | 'last command': command 254 | }) 255 | self._entity.schedule_update_ha_state() 256 | 257 | def attribute_updated(self, attribute, value): 258 | _LOGGER.debug('Attribute report received on cluster [0x%04x:%s:]=%s', 259 | self._cluster.cluster_id, 260 | attribute, 261 | value 262 | ) 263 | 264 | (attribute, value) = self._parse_attribute( 265 | self._entity, 266 | attribute, 267 | value, 268 | self._entity._model, 269 | cluster_id=self._cluster.cluster_id) 270 | try: 271 | if attribute == self._entity.value_attribute: 272 | self._entity._state = value 273 | except Exception: 274 | _LOGGER.debug("no default cluster defined") 275 | finally: 276 | if attribute == 0: 277 | self._entity._state = bool(value) 278 | self._entity.schedule_update_ha_state() 279 | 280 | 281 | class Server_Groups(Cluster_Server): 282 | def attribute_updated(self, attribute, value): 283 | _LOGGER.debug('Group report received: %s %s', attribute, value) 284 | 285 | 286 | class Server_LightLink(Cluster_Server): 287 | def __init__(self, entity, cluster, identifier): 288 | self._groups = list() 289 | super().__init__(entity, cluster, identifier) 290 | 291 | 292 | # def attribute_updated(self, attribute, value): 293 | # _LOGGER.debug('LightLink report received: %s %s', attribute, value) 294 | 295 | async def join_prepare(self, timeout=15): 296 | from .helpers import cluster_commisioning_groups 297 | _LOGGER.debug('LightLink prepare') 298 | try: 299 | self._entity._groups = self._groups = await cluster_commisioning_groups(self._cluster) 300 | _LOGGER.debug("discovered group from lightlink: %s", self._groups) 301 | except Exception as e: 302 | _LOGGER.debug( 303 | "catched exception in commissioning group_id %s", e) 304 | 305 | 306 | class Server_Scenes(Cluster_Server): 307 | def cluster_command(self, tsn, command_id, args): 308 | from zigpy.zcl.clusters.general import Scenes 309 | if tsn == self._prev_tsn: 310 | return 311 | self._prev_tsn = tsn 312 | command = Scenes.server_commands.get(command_id, ('unknown', ))[0] 313 | event_data = { 314 | 'entity_id': self._entity.entity_id, 315 | 'channel': self._identifier, 316 | 'command': command, 317 | self._identifier: args 318 | } 319 | self._entity.hass.bus.fire('click', event_data) 320 | _LOGGER.debug('click event [tsn:%s] %s', tsn, event_data) 321 | self._entity._device_state_attributes.update({ 322 | 'last seen': dt_util.now(), 323 | self._identifier: args, 324 | 'last command': command 325 | }) 326 | 327 | self._entity.schedule_update_ha_state() 328 | 329 | 330 | class Server_OccupancySensing(Cluster_Server): 331 | 332 | value_attribute = 0 333 | re_arm_sec = 20 334 | invalidate_after = None 335 | _state = 0 336 | 337 | def attribute_updated(self, attribute, value): 338 | """ handle trigger events from motion sensor. 339 | clear state after re_arm_sec seconds.""" 340 | _LOGGER.debug("Attribute received: %s %s", attribute, value) 341 | (attribute, value) = self._entity._parse_attribute(self._entity, attribute, value, self._entity._model, cluster_id=self._cluster.cluster_id) 342 | 343 | @asyncio.coroutine 344 | def _async_clear_state(entity): 345 | _LOGGER.debug("async_clear_state") 346 | if (entity.invalidate_after is None 347 | or entity.invalidate_after < dt_util.utcnow()): 348 | entity._entity._state = bool(0) 349 | entity._entity.schedule_update_ha_state() 350 | 351 | if attribute == self.value_attribute: 352 | self._entity._state = value 353 | self.invalidate_after = dt_util.utcnow() + datetime.timedelta( 354 | seconds=self.re_arm_sec) 355 | self._entity._device_state_attributes['last detection'] \ 356 | = dt_util.utcnow() 357 | async_track_point_in_time( 358 | self._entity.hass, _async_clear_state(self), 359 | self.invalidate_after) 360 | self._entity.hass.bus.fire('alarm', { 361 | 'entity_id': self._entity.entity_id, 362 | 'channel': self._identifier, 363 | 'command': "motion", 364 | }) 365 | 366 | self._entity.schedule_update_ha_state() 367 | 368 | 369 | class Server_TemperatureMeasurement(Cluster_Server): 370 | def attribute_updated(self, attribute, value): 371 | 372 | update_attrib = {} 373 | if attribute == 0: 374 | update_attrib['Temperature'] = round(float(value) / 100, 1) 375 | update_attrib['last seen'] = dt_util.now() 376 | self._entity._device_state_attributes.update(update_attrib) 377 | 378 | self._entity.schedule_update_ha_state() 379 | 380 | 381 | class Server_PowerConfiguration(Cluster_Server): 382 | def attribute_updated(self, attribute, value): 383 | update_attrib = {} 384 | _LOGGER.debug('Power report received: %s %s', attribute, value) 385 | if attribute == 0x20: 386 | update_attrib['Battery_Voltage'] = round(float(value) / 10, 1) 387 | elif attribute == 0x21: 388 | update_attrib['battery_level'] = value / 2 389 | elif attribute == 0x31: 390 | update_attrib['battery_type'] = BatteryType.get(value) 391 | update_attrib['last seen'] = dt_util.now() 392 | self._entity._device_state_attributes.update(update_attrib) 393 | # self._entity.schedule_update_ha_state() 394 | 395 | async def join_prepare(self, timeout=15): 396 | from .helpers import cluster_discover_attributes 397 | _LOGGER.debug('Power prepare') 398 | attribute_list = await cluster_discover_attributes(self._cluster, timeout) 399 | for attribute in attribute_list: 400 | if attribute.attrid == 0x20: 401 | await req_conf_report( 402 | self._cluster, 0x20, 60, 7200, 10 403 | ) 404 | elif attribute.attrid == 0x21: 405 | await req_conf_report( 406 | self._cluster, 0x21, 60, 7200, 10 407 | ) 408 | elif attribute.attrid == 0x31: 409 | result = await safe_read(self._cluster, ['battery_size']) 410 | if not result: 411 | return 412 | batterySize = result.get('battery_size') 413 | self._entity._device_state_attributes['battery_type'] = BatteryType.get(batterySize) 414 | # self._entity.schedule_update_ha_state() 415 | 416 | 417 | 418 | class Server_ElectricalMeasurement(Cluster_Server): 419 | 420 | # def attribute_updated(self, attribute, value): 421 | # pass 422 | 423 | async def join_prepare(self, timeout=15): 424 | _LOGGER.debug("measurement_type: %s", 'start') 425 | m_type = await safe_read(self._cluster, ['measurement_type']) 426 | if not m_type: 427 | _LOGGER.debug("measurement_type: %s", 'failed') 428 | return 429 | _LOGGER.debug("measurement_type: %x", m_type) 430 | self._entity._device_state_attributes['measurement_type'] = m_type.get('measurement_type') 431 | # self._entity.schedule_update_ha_state() 432 | 433 | async def async_update(self): 434 | _LOGGER.debug('[%s:0x%04x] cluster async_update called', 435 | self._entity.entity_id, 436 | self._cluster.cluster_id, 437 | ) 438 | # return 439 | attribute_list = await cluster_discover_attributes(self._cluster, 2, start = 0x0000) 440 | attributes = [a.attrid for a in attribute_list] 441 | return 442 | result = await safe_read(self._cluster, ['measurement_type']) 443 | if not result: 444 | _LOGGER.debug("measurement_type: %s", 'failed') 445 | return 446 | m_type=result.get('measurement_type') 447 | if m_type & 8: 448 | phaseA = await safe_read(self._cluster, attributes) 449 | # self._entity._device_state_attributes['active_power'] = active_power 450 | _LOGGER.debug("Phase A: %s", phaseA) 451 | 452 | 453 | class Server_Alarms(Cluster_Server): 454 | def attribute_updated(self, attribute, value): 455 | _LOGGER.info('Alarms report received: %s %s', attribute, value) 456 | 457 | # 458 | # update_attrib = {} 459 | # if attribute == 0: 460 | # update_attrib['Temperature'] = round(float(value) / 100, 1) 461 | # update_attrib['last seen'] = dt_util.now() 462 | # self._entity._device_state_attributes.update(update_attrib) 463 | # 464 | # self._entity.schedule_update_ha_state() 465 | # pass 466 | 467 | def init_clusters(entity, clusters): 468 | for (_, cluster) in clusters: 469 | if LevelControl.cluster_id == cluster.cluster_id: 470 | entity.sub_listener[cluster.cluster_id] = Server_LevelControl( 471 | entity, cluster, 'Level') 472 | elif OnOff.cluster_id == cluster.cluster_id: 473 | entity.sub_listener[cluster.cluster_id] = Server_OnOff( 474 | entity, cluster, 'OnOff') 475 | elif Scenes.cluster_id == cluster.cluster_id: 476 | entity.sub_listener[cluster.cluster_id] = Server_Scenes( 477 | entity, cluster, "Scenes") 478 | elif IasZone.cluster_id == cluster.cluster_id: 479 | entity.sub_listener[cluster.cluster_id] = Server_IasZone( 480 | entity, cluster, "IasZone") 481 | elif Basic.cluster_id == cluster.cluster_id: 482 | entity.sub_listener[cluster.cluster_id] = Server_Basic( 483 | entity, cluster, "Basic") 484 | elif OccupancySensing.cluster_id == cluster.cluster_id: 485 | entity.sub_listener[cluster.cluster_id] = Server_OccupancySensing( 486 | entity, cluster, "OccupancySensing") 487 | elif TemperatureMeasurement.cluster_id == cluster.cluster_id: 488 | entity.sub_listener[cluster.cluster_id] = Server_TemperatureMeasurement( 489 | entity, cluster, "TemperatureMeasurement") 490 | elif PowerConfiguration.cluster_id == cluster.cluster_id: 491 | entity.sub_listener[cluster.cluster_id] = Server_PowerConfiguration( 492 | entity, cluster, "PowerConfiguration") 493 | elif LightLink.cluster_id == cluster.cluster_id: 494 | entity.sub_listener[cluster.cluster_id] = Server_LightLink( 495 | entity, cluster, "LightLink") 496 | elif ElectricalMeasurement.cluster_id == cluster.cluster_id: 497 | entity.sub_listener[cluster.cluster_id] = Server_ElectricalMeasurement( 498 | entity, cluster, "ElectricalMeasurement") 499 | elif Alarms.cluster_id == cluster.cluster_id: 500 | entity.sub_listener[cluster.cluster_id] = Server_Alarms( 501 | entity, cluster, "Alarms") 502 | else: 503 | entity.sub_listener[cluster.cluster_id] = Cluster_Server( 504 | entity, cluster, cluster.cluster_id) 505 | -------------------------------------------------------------------------------- /custom_components/zha_new/const.py: -------------------------------------------------------------------------------- 1 | """All constants related to the ZHA_NEW component.""" 2 | import homeassistant.helpers.config_validation as cv 3 | import voluptuous as vol 4 | 5 | DOMAIN = 'zha_new' 6 | CONF_BAUDRATE = 'baudrate' 7 | CONF_DATABASE = 'database_path' 8 | CONF_DEVICE_CONFIG = 'device_config' 9 | CONF_USB_PATH = 'usb_path' 10 | DATA_DEVICE_CONFIG = 'zha_device_config' 11 | ENTITY_STORE = "entity_store" 12 | DEVICE_CLASS = {} 13 | SINGLE_CLUSTER_DEVICE_CLASS = {} 14 | COMPONENT_CLUSTERS = {} 15 | CONF_IN_CLUSTER = 'in_cluster' 16 | CONF_OUT_CLUSTER = 'out_cluster' 17 | CONF_CONFIG_REPORT = 'config_report' 18 | CONF_MANUFACTURER = 'manufacturer' 19 | CONF_MODEL = 'model' 20 | CONF_TEMPLATE = 'template' 21 | ATTR_DURATION = 'duration' 22 | ATTR_IEEE = 'ieee' 23 | ATTR_COMMAND = 'command' 24 | ATTR_ENTITY_ID = 'entity_id' 25 | ATTR_NWKID = "nwk" 26 | ATTR_STEP = 'step' 27 | 28 | SERVICE_PERMIT = 'permit' 29 | SERVICE_REMOVE = 'remove' 30 | SERVICE_COMMAND = 'command' 31 | SERVICE_MC_COMMAND = 'mc_command' 32 | SERVICE_COLORTEMP_STEP_UP = 'step_up_CT' 33 | SERVICE_COLORTEMP_STEP_DOWN = 'step_down_CT' 34 | SERVICE_COLORTEMP_STEP = 'step' 35 | 36 | 37 | # ZigBee definitions 38 | CENTICELSIUS = 'C-100' 39 | # Key in hass.data dict containing discovery info 40 | DISCOVERY_KEY = 'zha_discovery_info' 41 | BatteryType = { 42 | 0x00: "NoBattery", 43 | 0x01: "Built In", 44 | 0x02: "Other", 45 | 0x03: "AA", 46 | 0x04: "AAA", 47 | 0x05: "C", 48 | 0x06: "D", 49 | 0x07: "CR2", 50 | 0x08: "CR123A", 51 | 0xff: "Unknown", 52 | } 53 | 54 | 55 | # Internal definitions 56 | APPLICATION_CONTROLLER = None 57 | 58 | 59 | 60 | SERVICE_SCHEMAS = { 61 | SERVICE_PERMIT: vol.Schema({ 62 | vol.Optional(ATTR_DURATION, default=60): 63 | vol.All(vol.Coerce(int), vol.Range(0, 255)), 64 | }), 65 | SERVICE_REMOVE: vol.Schema({ 66 | vol.Optional(ATTR_IEEE, default=''): cv.string, 67 | vol.Optional(ATTR_NWKID): cv.positive_int, 68 | # vol.All(vol.Coerce(int), vol.Range(1, 65532)), 69 | }), 70 | SERVICE_COMMAND: vol.Schema({ 71 | ATTR_ENTITY_ID: cv.string, 72 | ATTR_COMMAND: cv.string, 73 | vol.Optional('cluster'): cv.positive_int, 74 | vol.Optional('attribute'): cv.positive_int, 75 | vol.Optional('value'): cv.positive_int, 76 | }), 77 | SERVICE_COLORTEMP_STEP: vol.Schema({ 78 | ATTR_ENTITY_ID: cv.comp_entity_ids, 79 | ATTR_STEP: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)) 80 | }), 81 | SERVICE_MC_COMMAND: vol.Schema({ 82 | 'group': cv.string, 83 | ATTR_COMMAND: cv.string, 84 | 'cluster': cv.positive_int, 85 | 'command': cv.string, 86 | vol.Optional('value'): cv.positive_int, 87 | }), 88 | } 89 | -------------------------------------------------------------------------------- /custom_components/zha_new/helpers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | #import zigpy.types as t 3 | import zigpy as z 4 | _LOGGER = logging.getLogger(__name__) 5 | from .const import ( 6 | DISCOVERY_KEY, 7 | DOMAIN, 8 | ) 9 | from homeassistant.helpers import discovery 10 | import sys 11 | import traceback 12 | async def cluster_discover_commands(cluster, timeout=2): 13 | 14 | cls_start = 0 15 | cls_no = 20 16 | command_list = list() 17 | while True: 18 | try: 19 | done, result = await cluster.discover_command_rec(cls_start, cls_no) 20 | _LOGGER.debug("[0x%04x:%s] discover_cluster_commands : %s", 21 | cluster._endpoint._device.nwk, cluster.cluster_id, result) 22 | command_list.extend(result).sort() 23 | if done: 24 | break 25 | else: 26 | cls_start = command_list[-1] + 1 27 | except TypeError: 28 | return 29 | except AttributeError: 30 | return 31 | except Exception as e: 32 | _LOGGER.debug( 33 | "[0x%04x:%s] catched exception in cluster_discover_commands %s", 34 | cluster._endpoint._device.nwk, cluster.cluster_id, 35 | e 36 | ) 37 | break 38 | _LOGGER.debug("[0x%04x:%s] discover_cluster_commands: %s", 39 | cluster._endpoint._device.nwk, cluster.cluster_id, 40 | command_list) 41 | return command_list 42 | 43 | 44 | async def cluster_discover_attributes(cluster, timeout, start=0x0000): 45 | 46 | cls_start = start 47 | cls_no = 20 48 | attribute_list = list() 49 | _LOGGER.debug("[0x%04x:%s] Start discover_cluster_attributes ", 50 | cluster._endpoint._device.nwk, cluster.cluster_id) 51 | while True: 52 | try: 53 | done, result = await cluster.discover_attributes(cls_start, cls_no) 54 | _LOGGER.debug("[0x%04x:%s] discover_cluster_attributes: %s", 55 | cluster._endpoint._device.nwk, cluster.cluster_id, 56 | result) 57 | attribute_list.extend(result) 58 | if done: 59 | break 60 | else: 61 | cls_start = attribute_list[-1] + 1 62 | # except TypeError: 63 | # return 64 | # except AttributeError: 65 | # return 66 | except Exception as e: 67 | _LOGGER.debug("[0x%04x:%s] catched exception in cluster_discover_attributes %s", 68 | cluster._endpoint._device.nwk, cluster.cluster_id, 69 | e) 70 | break 71 | _LOGGER.debug("[0x%04x:%sdiscover_attributes for %s: %s", 72 | cluster._endpoint._device.nwk, cluster.cluster_id, 73 | attribute_list) 74 | return attribute_list 75 | 76 | 77 | async def cluster_commisioning_groups(cluster, timeout=2): 78 | 79 | cls_start = 0 80 | cls_no = 0 81 | group_list = list() 82 | while True: 83 | total, cls_start, result = await cluster.get_group_identifier_request(cls_start) 84 | _LOGGER.debug("[0x%04x:%s] discover_group_identifier: %s", 85 | cluster._endpoint._device.nwk, cluster.cluster_id, 86 | result) 87 | group_list.extend(result) 88 | cls_start += cls_no 89 | if (cls_start + 1) >= total: 90 | break 91 | _LOGGER.debug("[0x%04x:%s] discover_commisioning_groups: %s", 92 | cluster._endpoint._device.nwk, cluster.cluster_id, 93 | group_list) 94 | 95 | return [group.GroupId for group in group_list] 96 | 97 | 98 | async def full_discovery(endpoint, timeout=5): 99 | commands = dict() 100 | attributes = dict() 101 | commands = dict() 102 | if 0x1000 in endpoint.in_clusters: 103 | #try: 104 | groups = await cluster_commisioning_groups(endpoint.in_clusters[0x1000], timeout=timeout) 105 | #except Exception as e: 106 | # _LOGGER.debug("catched exception in full_discovery group_id %s", e) 107 | 108 | for cluster_id, cluster in endpoint.in_clusters.items(): 109 | _LOGGER.debug("get information for cluster %s", cluster_id) 110 | try: 111 | commands[cluster_id] = await cluster_discover_commands(cluster, timeout=timeout) 112 | except Exception as e: 113 | _LOGGER.debug("catched exception in full_discovery %s", e) 114 | try: 115 | attributes[cluster_id] = await cluster_discover_attributes(cluster, timeout=timeout) 116 | except Exception as e: 117 | _LOGGER.debug("catched exception in full_discovery %s", e) 118 | 119 | async def create_MC_Entity(application, group_id): 120 | mdev = z.device(application, 0, group_id) 121 | mdev.add_endpoint(1) 122 | mdev.endpoint[1].profile = z.profiles.zha.PROFILE_ID 123 | for cluster_id in (0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0300): 124 | mdev.endpoint[1].in_clusters[cluster_id] = cluster \ 125 | = z.zcl.Cluster.from_id( 126 | mdev.add_endpoint[1], 127 | cluster_id 128 | ) 129 | if hasattr(cluster, 'ep_attribute'): 130 | mdev.endpoint[1]._cluster_attr[cluster.ep_attribute] = cluster 131 | discovery_info = { 132 | 'device': mdev, 133 | 'group_id': group_id, 134 | 'application': application, 135 | } 136 | device_key = "{}_MC_{}".format(DOMAIN, group_id) 137 | application._hass.data[DISCOVERY_KEY][device_key] = discovery_info 138 | await discovery.async_load_platform( 139 | application._hass, 140 | 'light', 141 | DOMAIN, 142 | {'discovery_key': device_key}, 143 | application._config, 144 | ) 145 | 146 | 147 | async def req_conf_report(report_cls, report_attr, report_min, report_max, report_change, mfgCode=None): 148 | from zigpy.zcl.foundation import Status 149 | 150 | endpoint = report_cls._endpoint 151 | try: 152 | v = await report_cls.bind() 153 | if v[0] > 0: 154 | _LOGGER.debug("[0x%04x:%s:0x%04x]: bind failed: %s", 155 | endpoint._device.nwk, 156 | endpoint.endpoint_id, 157 | report_cls.cluster_id, 158 | Status(v[0]).name) 159 | except Exception as e: 160 | _LOGGER.debug("[0x%04x:%s:0x%04x]: : bind exceptional failed %s", 161 | endpoint._device.nwk, 162 | endpoint.endpoint_id, 163 | report_cls.cluster_id, 164 | e) 165 | try: 166 | v = await report_cls.configure_reporting( 167 | report_attr, int(report_min), 168 | int(report_max), report_change, manufacturer=mfgCode) 169 | _LOGGER.debug("[0x%04x:%s:0x%04x] set config report status: %s", 170 | endpoint._device.nwk, 171 | endpoint._endpoint_id, 172 | report_cls.cluster_id, 173 | v) 174 | except Exception as e: 175 | _LOGGER.error("[0x%04x:%s:0x%04x] set config report exeptional failed: %s", 176 | endpoint._device.nwk, 177 | endpoint.endpoint_id, 178 | report_cls.cluster_id, 179 | e) 180 | 181 | async def safe_read(cluster, attributes): 182 | try: 183 | result, _ = await cluster.read_attributes( 184 | attributes, 185 | allow_cache=False, 186 | ) 187 | return result 188 | except Exception as e: # pylint: disable=broad-except 189 | exc_type, exc_value, exc_traceback = sys.exc_info() 190 | exep_data = traceback.format_exception(exc_type, exc_value, 191 | exc_traceback) 192 | for e in exep_data: 193 | _LOGGER.debug("> %s", e) 194 | _LOGGER.debug("[0x%04x:%s] safe_read failed: %s", 195 | cluster._endpoint._device.nwk, cluster.cluster_id, 196 | e) 197 | -------------------------------------------------------------------------------- /custom_components/zha_new/light.py: -------------------------------------------------------------------------------- 1 | """ 2 | Lights on Zigbee Home Automation networks. 3 | 4 | For more details on this platform, please refer to the documentation 5 | at https://home-assistant.io/components/light.zha/ 6 | 7 | """ 8 | import logging 9 | #import custom_components.zha_new.helpers as helpers 10 | import asyncio as a 11 | from .helpers import safe_read 12 | from homeassistant.components import light 13 | from homeassistant.const import ( 14 | STATE_UNKNOWN, 15 | ATTR_SUPPORTED_FEATURES 16 | ) 17 | import custom_components.zha_new as zha_new 18 | from importlib import import_module 19 | import homeassistant.util.color as color_util 20 | from zigpy.zcl.clusters.general import ( 21 | LevelControl, 22 | OnOff, 23 | Groups, 24 | Scenes, 25 | Basic) 26 | from zigpy.zcl.clusters.lighting import Color 27 | from custom_components.zha_new.cluster_handler import ( 28 | Cluster_Server) 29 | import homeassistant.util.dt as dt_util 30 | from .const import DOMAIN as PLATFORM 31 | from .const import ( 32 | SERVICE_SCHEMAS, 33 | SERVICE_COLORTEMP_STEP_UP, 34 | SERVICE_COLORTEMP_STEP_DOWN, 35 | SERVICE_COLORTEMP_STEP, 36 | ATTR_STEP, 37 | ) 38 | from homeassistant.components.light import DOMAIN 39 | _LOGGER = logging.getLogger(__name__) 40 | 41 | DEFAULT_DURATION = 0.5 42 | CAPABILITIES_COLOR_HUE = 0x01 43 | CAPABILITIES_COLOR_EXT_HUE = 0x02 44 | CAPABILITIES_COLOR_LOOP = 0x04 45 | CAPABILITIES_COLOR_XY = 0x08 46 | CAPABILITIES_COLOR_TEMP = 0x10 47 | UNSUPPORTED_ATTRIBUTE = 0x86 48 | 49 | 50 | async def async_setup_platform(hass, config, 51 | async_add_entities, discovery_info=None): 52 | """Set up the Zigbee Home Automation lights.""" 53 | discovery_info = zha_new.get_discovery_info(hass, discovery_info) 54 | if discovery_info is None: 55 | return 56 | 57 | application = discovery_info['application'] 58 | endpoint = discovery_info['endpoint'] 59 | in_clusters = discovery_info['in_clusters'] 60 | join = discovery_info['new_join'] 61 | component = hass.data[DOMAIN] 62 | # try: 63 | # discovery_info['color_capabilities'] \ 64 | # = await endpoint.light_color['color_capabilities'] 65 | # except AttributeError as e: 66 | # _LOGGER.debug("No color cluster: %s", e.args) 67 | # except KeyError as e: 68 | # _LOGGER.debug("Request for color_capabilities failed: %s", e.args) 69 | # except Exception as e: 70 | # _LOGGER.debug("Request for color_capabilities other error: %s", e.args) 71 | # entity = Light(**discovery_info) 72 | 73 | async def async_handle_step_up_ct_service(service): 74 | _LOGGER.debug('handle step up for %s: %s', service.data) 75 | 76 | component.async_register_entity_service( 77 | SERVICE_COLORTEMP_STEP_UP, SERVICE_SCHEMAS[SERVICE_COLORTEMP_STEP], 78 | 'async_handle_step_up_ct_service' 79 | ) 80 | component.async_register_entity_service( 81 | SERVICE_COLORTEMP_STEP_DOWN, SERVICE_SCHEMAS[SERVICE_COLORTEMP_STEP], 82 | 'async_handle_step_down_ct_service' 83 | ) 84 | if hasattr(discovery_info, 'multicast'): 85 | entity = MLight(**discovery_info) 86 | else: 87 | entity = Light(**discovery_info) 88 | if discovery_info['new_join']: 89 | for CH in entity.sub_listener.values(): 90 | await CH.join_prepare() 91 | 92 | e_registry = await hass.helpers.entity_registry.async_get_registry() 93 | reg_dev_id = e_registry.async_get_or_create( 94 | DOMAIN, PLATFORM, entity.uid, 95 | suggested_object_id=entity.entity_id, 96 | device_id=str(entity.device._ieee) 97 | ) 98 | if entity.entity_id != reg_dev_id.entity_id and 'unknown' in reg_dev_id.entity_id: 99 | _LOGGER.debug("entity has different name,change it: %s", reg_dev_id) 100 | e_registry.async_update_entity(reg_dev_id.entity_id, 101 | new_entity_id=entity.entity_id) 102 | if reg_dev_id.entity_id in application._entity_list: 103 | _LOGGER.debug("entity exist,remove it: %s", reg_dev_id) 104 | await application._entity_list.get(reg_dev_id.entity_id).async_remove() 105 | async_add_entities([entity]) 106 | 107 | entity_store = zha_new.get_entity_store(hass) 108 | if endpoint.device._ieee not in entity_store: 109 | entity_store[endpoint.device._ieee] = [] 110 | entity_store[endpoint.device._ieee].append(entity) 111 | if join: 112 | await auto_set_attribute_report(endpoint, in_clusters) 113 | endpoint._device._application.listener_event('device_updated', 114 | endpoint._device) 115 | 116 | 117 | class LightAttributeReports(Cluster_Server): 118 | current_x = None 119 | current_y = None 120 | 121 | def attribute_updated(self, attribute, value): 122 | _LOGGER.debug( 123 | "cluster:%s attribute=value received: %s=%s", 124 | self._cluster.cluster_id, attribute, 125 | value, 126 | ) 127 | if self._entity._call_ongoing is True: 128 | return 129 | if self._cluster.cluster_id == OnOff.cluster_id: 130 | if attribute == 0: 131 | self._entity._state = True if value else False 132 | # self._entity.schedule_update_ha_state() 133 | if self._entity.is_on: 134 | if self._cluster.cluster_id == LevelControl.cluster_id: 135 | if attribute == 0: 136 | self._entity._brightness = value 137 | _LOGGER.debug( 138 | "cluster:%s attribute=value processed %s=%s", 139 | self._cluster.cluster_id, 140 | attribute, 141 | value, 142 | ) 143 | if self._cluster.cluster_id == Color.cluster_id: 144 | if attribute == 3: 145 | self.current_x = value 146 | self._entity._hs_color = (self.current_x, self.current_y) 147 | elif attribute == 4: 148 | self.current_y == value 149 | self._entity._hs_color = (self.current_x, self.current_y) 150 | elif attribute == 7: 151 | self._entity._color_temp = value 152 | self._entity.schedule_update_ha_state() 153 | 154 | 155 | class Light(zha_new.Entity, light.Light): 156 | 157 | """Representation of a ZHA or ZLL light.""" 158 | 159 | _domain = DOMAIN 160 | 161 | def __init__(self, **kwargs): 162 | """Initialize the ZHA light.""" 163 | super().__init__(**kwargs) 164 | 165 | in_clusters = kwargs['in_clusters'] 166 | out_clusters = kwargs['out_clusters'] 167 | endpoint = kwargs['endpoint'] 168 | self._available = True 169 | self._assumed = False 170 | # self._groups = None 171 | self._grp_name = None 172 | self._supported_features = 0 173 | self._color_temp = None 174 | self._hs_color = None 175 | self._brightness = None 176 | self._current_x = None 177 | self._current_y = None 178 | self._color_temp_physical_min = None 179 | self._color_temp_physical_max = None 180 | self._call_ongoing = False 181 | self.CurrentHue = None 182 | self.CurrentSaturation = None 183 | self.EnhancedCurrentHue = None 184 | self._caps = 0 185 | 186 | if Groups.cluster_id in self._in_clusters: 187 | self._groups = list() 188 | self._device_state_attributes["Group_id"] = self._groups 189 | 190 | clusters = list(out_clusters.items()) + list(in_clusters.items()) 191 | _LOGGER.debug("[0x%04x:%s] initialize cluster listeners: (%s/%s) ", 192 | endpoint._device.nwk, 193 | endpoint.endpoint_id, 194 | list(in_clusters.keys()), list(out_clusters.keys())) 195 | for (_, cluster) in clusters: 196 | self.sub_listener[cluster.cluster_id] = LightAttributeReports( 197 | self, cluster, cluster.cluster_id) 198 | 199 | endpoint._device.zdo.add_listener(self) 200 | 201 | async def async_handle_step_up_ct_service(self, *args, **kwargs): 202 | _LOGGER.debug('handle step up for Class Light %s: %s - %s', self.entity_id, args, kwargs) 203 | step = kwargs.get(ATTR_STEP, None) 204 | if not (step and hasattr(self._endpoint, 'light_color')): 205 | return False 206 | duration = kwargs.get(light.ATTR_TRANSITION, DEFAULT_DURATION) 207 | self._call_ongoing = True 208 | await self._endpoint.light_color.step_color_temp( 209 | 0x01, step, int(duration/10), 0, 0) 210 | await a.sleep(duration) 211 | self._call_ongoing = False 212 | return True 213 | 214 | async def async_handle_step_down_ct_service(self, *args, **kwargs): 215 | _LOGGER.debug('handle step up for Class Light %s: %s - %s', self.entity_id, args, kwargs) 216 | step = kwargs.get(ATTR_STEP, None) 217 | if not (step and hasattr(self._endpoint, 'light_color')): 218 | return False 219 | duration = kwargs.get(light.ATTR_TRANSITION, DEFAULT_DURATION) 220 | self._call_ongoing = True 221 | await self._endpoint.light_color.step_color_temp( 222 | 0x03, step, int(duration/10), 0, 0) 223 | await a.sleep(duration) 224 | self._call_ongoing = False 225 | return True 226 | 227 | @property 228 | def is_on(self) -> bool: 229 | """Return true if entity is on.""" 230 | if self._state == STATE_UNKNOWN: 231 | return False 232 | return bool(self._state) 233 | 234 | @property 235 | def assumed_state(self) -> bool: 236 | """Return True if unable to access real state of the entity.""" 237 | return bool(self._assumed) 238 | 239 | async def async_turn_on(self, **kwargs): 240 | _LOGGER.debug("turn_on with %s", kwargs) 241 | self._call_ongoing = True 242 | """Turn the entity on.""" 243 | duration = kwargs.get(light.ATTR_TRANSITION, DEFAULT_DURATION) 244 | duration = duration * 10 # tenths of s 245 | if light.ATTR_COLOR_TEMP in kwargs: 246 | temperature = kwargs[light.ATTR_COLOR_TEMP] 247 | await self._endpoint.light_color.move_to_color_temp( 248 | temperature, duration) 249 | self._color_temp = temperature 250 | 251 | if light.ATTR_HS_COLOR in kwargs: 252 | self._hs_color = kwargs[light.ATTR_HS_COLOR] 253 | xy_color = color_util.color_hs_to_xy(*self._hs_color) 254 | await self._endpoint.light_color.move_to_color( 255 | int(xy_color[0] * 65535), 256 | int(xy_color[1] * 65535), 257 | duration, 258 | ) 259 | 260 | if light.ATTR_BRIGHTNESS in kwargs: 261 | brightness = kwargs.get( 262 | light.ATTR_BRIGHTNESS, self._brightness) 263 | self._brightness = 2 if (brightness < 2) else brightness 264 | _LOGGER.debug("[0x%04x:%s] move_to_level_w_onoff: %s ", 265 | self._endpoint._device.nwk, 266 | self._endpoint.endpoint_id, 267 | self._brightness) 268 | 269 | await self._endpoint.level.move_to_level_with_on_off( 270 | self._brightness, 271 | duration 272 | ) 273 | self._state = True 274 | else: 275 | await self._endpoint.on_off.on() 276 | self._state = True 277 | await a.sleep(duration/10) 278 | self._call_ongoing = False 279 | 280 | async def async_turn_off(self, **kwargs): 281 | """Turn the entity off.""" 282 | await self._endpoint.on_off.off() 283 | self._state = False 284 | # self.async_schedule_update_ha_state() 285 | 286 | @property 287 | def brightness(self): 288 | """Return the brightness of this light between 0..255.""" 289 | _LOGGER.debug("polled state brightness: %s", self._brightness) 290 | return self._brightness 291 | 292 | @property 293 | def xy_color(self): 294 | """Return the XY color value [float, float].""" 295 | return self._xy_color 296 | 297 | @property 298 | def color_temp(self): 299 | """Return the CT color value in mireds.""" 300 | return self._color_temp 301 | 302 | @property 303 | def supported_features(self): 304 | """Flag supported features.""" 305 | return self._supported_features 306 | 307 | async def async_update(self): 308 | """Retrieve latest state.""" 309 | _LOGGER.debug("%s async_update", self.entity_id) 310 | 311 | try: 312 | result, _ = await self._endpoint.on_off.read_attributes( 313 | ['on_off'], 314 | allow_cache=False, 315 | ) 316 | _LOGGER.debug(" poll received for %s : %s", self.entity_id, result) 317 | except Exception as e: 318 | _LOGGER.debug('poll for %s failed: %s', self.entity_id, e) 319 | result = None 320 | try: 321 | self._state = result['on_off'] 322 | self._assumed = False 323 | # _LOGGER.debug("assumed state for %s is false", self.entity_id) 324 | self._device_state_attributes.update({ 325 | 'last seen': dt_util.now(), 326 | }) 327 | except Exception: 328 | self._assumed = True 329 | return 330 | 331 | if self._groups is not None: 332 | try: 333 | result = await self._endpoint.groups.get_membership([]) 334 | _LOGGER.debug("%s get membership : %s", self.entity_id, result) 335 | except Exception as e: 336 | result = None 337 | _LOGGER.debug( 338 | "%s get membership failed: %s", 339 | self.entity_id, 340 | e, 341 | ) 342 | if result: 343 | if result[0] >= 1: 344 | self._groups = result[1] 345 | if (self._device_state_attributes.get("Group_id") 346 | != self._groups): 347 | self._device_state_attributes["Group_id"] = self._groups 348 | for groups in self._groups: 349 | self._endpoint._device._application.listener_event( 350 | 'subscribe_group', 351 | groups) 352 | if self.is_on: 353 | if self._supported_features & light.SUPPORT_BRIGHTNESS: 354 | result = await safe_read(self._endpoint.level, 355 | ['current_level']) 356 | if result: 357 | self._brightness = result.get( 358 | 'current_level', self._brightness 359 | ) 360 | _LOGGER.debug("poll brightness %s", self._brightness) 361 | 362 | if self._supported_features & light.SUPPORT_COLOR_TEMP: 363 | result = await safe_read(self._endpoint.light_color, 364 | ['color_temperature']) 365 | if result: 366 | self._color_temp = result.get('color_temperature', 367 | self._color_temp) 368 | 369 | if self._supported_features & light.SUPPORT_COLOR: 370 | result = await safe_read(self._endpoint.light_color, 371 | ['current_x', 'current_y']) 372 | if result: 373 | if 'current_x' in result and 'current_y' in result: 374 | self._hs_color = ( 375 | result['current_x'], result['current_y'] 376 | ) 377 | 378 | @property 379 | def should_poll(self) -> bool: 380 | """Return True if entity has to be polled for state. 381 | False if entity pushes its state to HA. 382 | """ 383 | return True 384 | 385 | def cluster_command(self, tsn, command_id, args): 386 | try: 387 | dev_func = self._model.replace(".", "_").replace(" ", "_") 388 | _custom_cluster_command = getattr( 389 | import_module("custom_components.device." + dev_func), 390 | "_custom_cluster_command" 391 | ) 392 | _custom_cluster_command(self, tsn, command_id, args) 393 | except ImportError as e: 394 | _LOGGER.debug("Import DH %s failed: %s", dev_func, e.args) 395 | except Exception as e: 396 | _LOGGER.info("Excecution of DH %s failed: %s", dev_func, e.args) 397 | 398 | def device_announce(self, *args, **kwargs): 399 | a.ensure_future( 400 | auto_set_attribute_report(self._endpoint, self._in_clusters) 401 | ) 402 | a.ensure_future(self.async_update()) 403 | a.ensure_future(self._get_caps_features()) 404 | self._assumed = False 405 | _LOGGER.debug( 406 | "0x%04x device announce for light received", 407 | self._endpoint._device.nwk, 408 | ) 409 | 410 | @property 411 | def max_mireds(self): 412 | return ( 413 | self._color_temp_physical_max 414 | if self._color_temp_physical_max 415 | else 500) 416 | 417 | @property 418 | def min_mireds(self): 419 | return ( 420 | self._color_temp_physical_min 421 | if self._color_temp_physical_min 422 | else 153) 423 | 424 | async def get_range_mired(self): 425 | result = await safe_read( 426 | self._endpoint.light_color, 427 | ['color_temp_physical_min', 'color_temp_physical_max'], 428 | ) 429 | if result: 430 | self._color_temp_physical_min = result.get( 431 | 'color_temp_physical_min', None 432 | ) 433 | self._color_temp_physical_max = result.get( 434 | 'color_temp_physical_max', None 435 | ) 436 | 437 | async def async_added_to_hass(self): 438 | """Call when entity about to be added to hass.""" 439 | await super().async_added_to_hass() 440 | try: 441 | self._supported_features = self._restore_data.attributes[ATTR_SUPPORTED_FEATURES] 442 | except Exception: 443 | await self._get_caps_features() 444 | 445 | async def _get_caps_features(self): 446 | self._caps = 0 447 | if hasattr(self._endpoint, 'light_color'): 448 | try: 449 | self._caps = await safe_read( 450 | self._endpoint.light_color, ['color_capabilities']).get( 451 | 'color_capabilities') 452 | except AttributeError: 453 | self._caps = CAPABILITIES_COLOR_XY 454 | try: 455 | result = await safe_read( 456 | self._endpoint.light_color, ['color_temperature']) 457 | if result.get('color_temperature') is not UNSUPPORTED_ATTRIBUTE: 458 | self._caps = self._caps | CAPABILITIES_COLOR_TEMP 459 | except AttributeError: 460 | pass 461 | 462 | if self._caps & CAPABILITIES_COLOR_TEMP: 463 | self._supported_features |= light.SUPPORT_COLOR_TEMP 464 | a.ensure_future(self.get_range_mired()) # TODO move to new join only 465 | if (self._caps & CAPABILITIES_COLOR_HUE) or (self._caps & CAPABILITIES_COLOR_EXT_HUE) or (self._caps & CAPABILITIES_COLOR_XY): 466 | self._supported_features |= light.SUPPORT_COLOR 467 | self._hs_color = (0, 0) 468 | if LevelControl.cluster_id in self._in_clusters: 469 | self._supported_features |= light.SUPPORT_BRIGHTNESS 470 | self._supported_features |= light.SUPPORT_TRANSITION 471 | try: 472 | self._brightness = self._restore_data.attributes[light.ATTR_BRIGHTNESS] 473 | except Exception: 474 | pass 475 | 476 | 477 | async def auto_set_attribute_report(endpoint, in_clusters): 478 | _LOGGER.debug( 479 | "[0x%04x:%s] called to set reports", 480 | endpoint._device.nwk, 481 | endpoint.endpoint_id, 482 | ) 483 | 484 | if 0x0006 in in_clusters: 485 | await zha_new.req_conf_report( 486 | endpoint.in_clusters[0x0006], 0, 1, 600, 1 487 | ) 488 | if 0x0008 in in_clusters: 489 | await zha_new.req_conf_report( 490 | endpoint.in_clusters[0x0008], 0, 1, 600, 1 491 | ) 492 | if 0x0300 in in_clusters: 493 | await zha_new.req_conf_report( 494 | endpoint.in_clusters[0x0300], 3, 1, 600, 1 495 | ) 496 | await zha_new.req_conf_report( 497 | endpoint.in_clusters[0x0300], 4, 1, 600, 1 498 | ) 499 | await zha_new.req_conf_report( 500 | endpoint.in_clusters[0x0300], 7, 1, 600, 1 501 | ) 502 | -------------------------------------------------------------------------------- /custom_components/zha_new/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "zha_new", 3 | "name": "zha new", 4 | "requirements": ["https://github.com/Yoda-x/bellows/archive/master.zip#bellows==100.7.4.12", 5 | "https://github.com/Yoda-x/zigpy/archive/master.zip#zigpy==100.1.4.10"], 6 | "documentation": "github", 7 | "dependencies": [], 8 | "codeowners": ["Yoda-x"], 9 | } 10 | -------------------------------------------------------------------------------- /custom_components/zha_new/sensor.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Sensors on Zigbee Home Automation networks. 4 | 5 | For more details on this platform, please refer to the documentation 6 | at https://home-assistant.io/components/sensor.zha/ 7 | 8 | """ 9 | import logging 10 | from homeassistant.components.sensor import DOMAIN 11 | from homeassistant.const import ( 12 | STATE_UNKNOWN, 13 | TEMP_CELSIUS, 14 | ) 15 | from custom_components.zha_new.cluster_handler import init_clusters 16 | import custom_components.zha_new as zha_new 17 | from asyncio import ensure_future 18 | from .const import DOMAIN as PLATFORM 19 | _LOGGER = logging.getLogger(__name__) 20 | 21 | 22 | async def async_setup_platform( 23 | hass, config, async_add_devices, discovery_info=None): 24 | from zigpy.zcl.clusters.security import IasZone 25 | """Set up Zigbee Home Automation sensors.""" 26 | discovery_info = zha_new.get_discovery_info(hass, discovery_info) 27 | if discovery_info is None: 28 | return 29 | endpoint = discovery_info['endpoint'] 30 | in_clusters = discovery_info['in_clusters'] 31 | application = discovery_info['application'] 32 | 33 | """ create ias cluster if it not already exists""" 34 | if IasZone.cluster_id not in in_clusters: 35 | cluster = endpoint.add_input_cluster(IasZone.cluster_id) 36 | in_clusters[IasZone.cluster_id] = cluster 37 | endpoint.in_clusters[IasZone.cluster_id] = cluster 38 | else: 39 | cluster = in_clusters[IasZone.cluster_id] 40 | 41 | if discovery_info['new_join']: 42 | try: 43 | await cluster.bind() 44 | ieee = cluster.endpoint.device.application.ieee 45 | await cluster.write_attributes({'cie_addr': ieee}) 46 | _LOGGER.debug("write cie done") 47 | except: 48 | _LOGGER.debug("bind/write cie failed") 49 | 50 | entity = await make_sensor(discovery_info) 51 | if discovery_info['new_join']: 52 | for CH in entity.sub_listener.values(): 53 | await CH.join_prepare() 54 | 55 | _LOGGER.debug("Create sensor.zha: %s", entity.entity_id) 56 | e_registry = await hass.helpers.entity_registry.async_get_registry() 57 | reg_dev_id = e_registry.async_get_or_create( 58 | DOMAIN, PLATFORM, entity.uid, 59 | suggested_object_id=entity.entity_id, 60 | device_id=str(entity.device._ieee) 61 | ) 62 | if entity.entity_id != reg_dev_id.entity_id and 'unknown' in reg_dev_id.entity_id: 63 | _LOGGER.debug("entity different name,change it: %s", reg_dev_id) 64 | e_registry.async_update_entity(reg_dev_id.entity_id, 65 | new_entity_id=entity.entity_id) 66 | if reg_dev_id.entity_id in application._entity_list: 67 | _LOGGER.debug("entity exist,remove it: %s", reg_dev_id) 68 | await application._entity_list.get(reg_dev_id.entity_id).async_remove() 69 | async_add_devices([entity]) 70 | 71 | endpoint._device._application.listener_event( 72 | 'device_updated', endpoint._device) 73 | entity_store = zha_new.get_entity_store(hass) 74 | if endpoint.device._ieee not in entity_store: 75 | entity_store[endpoint.device._ieee] = [] 76 | entity_store[endpoint.device._ieee].append(entity) 77 | 78 | 79 | async def make_sensor(discovery_info): 80 | """Create ZHA sensors factory.""" 81 | from zigpy.zcl.clusters.measurement import TemperatureMeasurement 82 | from zigpy.zcl.clusters.measurement import RelativeHumidity 83 | from zigpy.zcl.clusters.measurement import PressureMeasurement 84 | from zigpy.zcl.clusters.measurement import IlluminanceMeasurement 85 | from zigpy.zcl.clusters.smartenergy import Metering 86 | from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement 87 | 88 | in_clusters = discovery_info['in_clusters'] 89 | endpoint = discovery_info['endpoint'] 90 | 91 | if TemperatureMeasurement.cluster_id in in_clusters: 92 | sensor = TemperatureSensor(**discovery_info, 93 | cluster_key=TemperatureMeasurement.ep_attribute) 94 | elif RelativeHumidity.cluster_id in in_clusters: 95 | sensor = HumiditySensor(**discovery_info, 96 | cluster_key=RelativeHumidity.ep_attribute) 97 | elif PressureMeasurement.cluster_id in in_clusters: 98 | sensor = PressureSensor(**discovery_info, 99 | cluster_key=PressureMeasurement.ep_attribute) 100 | elif Metering.cluster_id in in_clusters: 101 | sensor = MeteringSensor(**discovery_info, 102 | cluster_key=Metering.ep_attribute) 103 | elif IlluminanceMeasurement.cluster_id in in_clusters: 104 | sensor = IlluminanceSensor(**discovery_info, 105 | cluster_key=IlluminanceMeasurement.ep_attribute) 106 | elif ElectricalMeasurement.cluster_id in in_clusters: 107 | sensor = ElectricalMeasurementSensor(**discovery_info, 108 | cluster_key=ElectricalMeasurement.ep_attribute) 109 | else: 110 | sensor = Sensor(**discovery_info) 111 | 112 | _LOGGER.debug("Return make_sensor - %s", endpoint._device._ieee) 113 | return sensor 114 | 115 | 116 | class Sensor(zha_new.Entity): 117 | 118 | """Base ZHA sensor.""" 119 | 120 | _domain = DOMAIN 121 | value_attribute = 0 122 | min_reportable_change = 1 123 | state_div = 1 124 | state_prec = 1 125 | 126 | def __init__(self, **kwargs): 127 | super().__init__(**kwargs) 128 | 129 | endpoint = kwargs['endpoint'] 130 | in_clusters = kwargs['in_clusters'] 131 | out_clusters = kwargs['out_clusters'] 132 | clusters = list(out_clusters.items()) + list(in_clusters.items()) 133 | _LOGGER.debug("[0x%04x:%s] initialize cluster listeners: (%s/%s) ", 134 | endpoint._device.nwk, 135 | endpoint.endpoint_id, 136 | list(in_clusters.keys()), list(out_clusters.keys())) 137 | 138 | init_clusters(self, clusters) 139 | endpoint._device.zdo.add_listener(self) 140 | 141 | def attribute_updated(self, attribute, value): 142 | 143 | (attribute, value) = self._parse_attribute( 144 | self, attribute, value, self._model) 145 | if attribute == self.value_attribute: 146 | self._state = value 147 | self.schedule_update_ha_state() 148 | 149 | @property 150 | def state(self): 151 | """Return the state of the entity.""" 152 | if self._state is None: 153 | return STATE_UNKNOWN 154 | value = round(float(self._state) / self.state_div, self.state_prec) 155 | return value 156 | 157 | def device_announce(self, *args, **kwargs): 158 | 159 | _LOGGER.debug( 160 | "0x%04x device announce for sensor received", 161 | self._endpoint._device.nwk, 162 | ) 163 | 164 | @property 165 | def should_poll(self) -> bool: 166 | """Return True if entity has to be polled for state. 167 | False if entity pushes its state to HA. 168 | """ 169 | return False 170 | 171 | 172 | 173 | 174 | class TemperatureSensor(Sensor): 175 | 176 | """ZHA temperature sensor.""" 177 | 178 | min_reportable_change = 20 179 | state_div = 100 180 | 181 | def __init__(self, **kwargs): 182 | super().__init__(**kwargs) 183 | self._device_class = 'temperature' 184 | 185 | @property 186 | def unit_of_measurement(self): 187 | """Return the unit of measurement of this entity.""" 188 | return TEMP_CELSIUS 189 | 190 | # @property 191 | # def state(self): 192 | # """Return the state of the entity.""" 193 | # if self._state is None: 194 | # return '-' 195 | # celsius = round(float(self._state) / self.state_div, self.state_prec) 196 | # return convert_temperature( 197 | # celsius, TEMP_CELSIUS, self.unit_of_measurement) 198 | 199 | 200 | class HumiditySensor(Sensor): 201 | 202 | """ZHA humidity sensor.""" 203 | 204 | state_div = 100 205 | 206 | def __init__(self, **kwargs): 207 | super().__init__(**kwargs) 208 | self._device_class = 'humidity' 209 | 210 | @property 211 | def unit_of_measurement(self): 212 | """Return the unit of measuremnt of this entity.""" 213 | return "%" 214 | 215 | # @property 216 | # def state(self): 217 | # """Return the state of the entity.""" 218 | # if self._state is None: 219 | # return '-' 220 | # percent = round(float(self._state) / 100, 1) 221 | # return percent 222 | 223 | 224 | class PressureSensor(Sensor): 225 | 226 | """ZHA pressure sensor.""" 227 | 228 | def __init__(self, **kwargs): 229 | super().__init__(**kwargs) 230 | self._device_class = 'pressure' 231 | 232 | min_reportable_change = 50 233 | 234 | @property 235 | def unit_of_measurement(self): 236 | """Return the unit of measuremnt of this entity.""" 237 | return "hPa" 238 | 239 | # @property 240 | # def state(self): 241 | # """Return the state of the entity.""" 242 | # if self._state is None: 243 | # return '-' 244 | # return self._state 245 | 246 | 247 | class IlluminanceSensor(Sensor): 248 | 249 | """ZHA pressure sensor.""" 250 | 251 | min_reportable_change = 5 252 | 253 | def __init__(self, **kwargs): 254 | super().__init__(**kwargs) 255 | self._device_class = 'illuminance' 256 | 257 | @property 258 | def unit_of_measurement(self): 259 | """Return the unit of measuremnt of this entity.""" 260 | return "lx" 261 | 262 | # @property 263 | # def state(self): 264 | # """Return the state of the entity.""" 265 | # if self._state is None: 266 | # return None 267 | # return self._state 268 | 269 | class ElectricalMeasurementSensor(Sensor): 270 | 271 | def __init__(self, **kwargs): 272 | super().__init__(**kwargs) 273 | self._device_class = 'ElectricalMeasurement' 274 | 275 | @property 276 | def should_poll(self) -> bool: 277 | """Return True if entity has to be polled for state. 278 | False if entity pushes its state to HA. 279 | """ 280 | return True 281 | 282 | async def async_update(self): 283 | _LOGGER.debug('[%s] Entity async_update called', 284 | self.entity_id, 285 | ) 286 | for CH in self.sub_listener.values(): 287 | await CH.async_update() 288 | 289 | 290 | class MeteringSensor(Sensor): 291 | 292 | """ZHA smart engery metering.""" 293 | 294 | value_attribute = 0 295 | state_div = 100 296 | state_prec = 2 297 | 298 | def __init__(self, **kwargs): 299 | super().__init__(**kwargs) 300 | self.meter_cls = self._endpoint.in_clusters[0x0702] 301 | 302 | @property 303 | def unit_of_measurement(self): 304 | """Return the unit of measuremnt of this entity.""" 305 | return "kWh" 306 | 307 | # @property 308 | # def state(self): 309 | # """Return the state of the entity.""" 310 | # if self._state is None: 311 | # return "-" 312 | # kwh = round(float(self._state) / 100, 2) 313 | # return kwh 314 | 315 | @property 316 | def should_poll(self) -> bool: 317 | """Return True if entity has to be polled for state. 318 | False if entity pushes its state to HA. 319 | """ 320 | return False 321 | 322 | async def async_update(self): 323 | """Retrieve latest state.""" 324 | _LOGGER.debug('[0x%04x] Entity async_update called', 325 | self.nwk, 326 | ) 327 | # ptr=0 328 | # #_LOGGER.debug("%s async_update", self.entity_id) 329 | # # while len_v==1: 330 | # v = yield from self.meter_cls.discover_attributes(0, 32) 331 | attribs = [0, ] 332 | # for item in v[0]: 333 | # self.meter_attributes[item.attrid]=item.datatype 334 | # ptr=item.attrid + 1 if item.attrid > ptr else ptr 335 | # attribs.extend(list(self.meter_attributes.keys())) 336 | # # _LOGGER.debug("query %s:", attribs) 337 | # #v = yield from self.meter_cls.read_attributes_raw(attribs) 338 | v = await self.meter_cls.read_attributes(attribs) 339 | # # _LOGGER.debug("attributes for cluster:%s" , v[0]) 340 | for attrid, value in v[0].items(): 341 | if attrid == 0: 342 | self._state = value 343 | # attrid_record=Metering.attributes.get(attrid,None ) 344 | # if attrid_record: 345 | # self._device_state_attributes[attrid_record[0]] = value 346 | # else: 347 | # self._device_state_attributes["metering_"+str(attrid)] = value 348 | # #self._state = v[0].value 349 | 350 | def cluster_command(self, tsn, command_id, args): 351 | """Handle commands received to this cluster.""" 352 | _LOGGER.debug("sensor cluster_command %s", command_id) 353 | 354 | def device_announce(self, *args, **kwargs): 355 | 356 | ensure_future( 357 | auto_set_attribute_report(self._endpoint, self._in_clusters) 358 | ) 359 | ensure_future(self.async_update()) 360 | self._assumed = False 361 | _LOGGER.debug("0x%04x device announce for sensor received", 362 | self._endpoint._device.nwk) 363 | 364 | 365 | async def auto_set_attribute_report(endpoint, in_clusters): 366 | _LOGGER.debug( 367 | "[0x%04x:%s] called to set reports", 368 | endpoint._device.nwk, endpoint.endpoint_id) 369 | 370 | if 0x0702 in in_clusters: 371 | await zha_new.req_conf_report( 372 | endpoint.in_clusters[0x0702], 0, 1, 600, 1) 373 | -------------------------------------------------------------------------------- /custom_components/zha_new/services.yaml: -------------------------------------------------------------------------------- 1 | # Describes the format for available zha services 2 | 3 | permit: 4 | description: Allow nodes to join the ZigBee network. 5 | fields: 6 | duration: 7 | description: Time to permit joins, in seconds 8 | example: 60 9 | 10 | remove: 11 | description: remove node(s) from the ZigBee network. 12 | fields: 13 | ieee: 14 | description: ieee or parts of it 15 | example: 00:15:8d:00:01:f4:66:c1 16 | nwk: 17 | description: nwk id of the device 18 | example: 1234 19 | 20 | command: 21 | description: send a zigbee command to an entity, command write_attribute, tbd 22 | fields: 23 | entity_id: 24 | description: entity_id 25 | command: 26 | description: write_attribute 27 | example: valid commands write_attribute 28 | cluster: 29 | description: cluster 30 | attribute: 31 | description: attribute 32 | value: 33 | description: value 34 | mfgid: 35 | description: optional manufacturer ID 36 | 37 | step_up_ct: 38 | description: send a colortemp step up command to a zigbee bulb 39 | fields: 40 | entity_id: 41 | description: entity_id 42 | step: 43 | description: value for step up, 0-255 44 | -------------------------------------------------------------------------------- /custom_components/zha_new/switch.py: -------------------------------------------------------------------------------- 1 | """Switches on Zigbee Home Automation networks.""" 2 | 3 | import logging 4 | from asyncio import ensure_future 5 | from homeassistant.components.switch import DOMAIN, SwitchDevice 6 | import custom_components.zha_new as zha_new 7 | from custom_components.zha_new.cluster_handler import ( 8 | Cluster_Server, 9 | Server_OnOff, 10 | Server_Scenes, 11 | Server_Basic, 12 | Server_Groups, 13 | ) 14 | from importlib import import_module 15 | from zigpy.zcl.clusters.general import ( 16 | OnOff, 17 | Groups, 18 | Scenes, 19 | Basic) 20 | #from zigpy.exceptions import DeliveryError 21 | from .const import DOMAIN as PLATFORM 22 | _LOGGER = logging.getLogger(__name__) 23 | 24 | """ change to zha-new for use in home dir """ 25 | #DEPENDENCIES = ['zha_new'] 26 | 27 | 28 | def setup_platform( 29 | hass, config, async_add_devices, discovery_info=None): 30 | _LOGGER.debug("disocery info setup_platform: %s", discovery_info) 31 | return True 32 | 33 | 34 | async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): 35 | """Set up Zigbee Home Automation switches.""" 36 | discovery_info = zha_new.get_discovery_info(hass, discovery_info) 37 | if discovery_info is None: 38 | return 39 | 40 | application = discovery_info['application'] 41 | endpoint = discovery_info['endpoint'] 42 | in_clusters = discovery_info['in_clusters'] 43 | join = discovery_info['new_join'] 44 | 45 | entity = Switch(**discovery_info) 46 | if discovery_info['new_join']: 47 | for CH in entity.sub_listener.values(): 48 | await CH.join_prepare() 49 | e_registry = await hass.helpers.entity_registry.async_get_registry() 50 | reg_dev_id = e_registry.async_get_or_create( 51 | DOMAIN, PLATFORM, entity.uid, 52 | suggested_object_id=entity.entity_id, 53 | device_id=str(entity.device._ieee) 54 | ) 55 | if entity.entity_id != reg_dev_id.entity_id and 'unknown' in reg_dev_id.entity_id: 56 | _LOGGER.debug("entity different name,change it: %s", reg_dev_id) 57 | e_registry.async_update_entity(reg_dev_id.entity_id, 58 | new_entity_id=entity.entity_id) 59 | if reg_dev_id.entity_id in application._entity_list: 60 | _LOGGER.debug("entity exist,remove it: %s", reg_dev_id) 61 | await application._entity_list.get(reg_dev_id.entity_id).async_remove() 62 | async_add_devices([entity]) 63 | if join: 64 | await auto_set_attribute_report(endpoint, in_clusters) 65 | entity_store = zha_new.get_entity_store(hass) 66 | 67 | if endpoint.device._ieee not in entity_store: 68 | entity_store[endpoint.device._ieee] = [] 69 | entity_store[endpoint.device._ieee].append(entity) 70 | 71 | 72 | class Switch(zha_new.Entity, SwitchDevice): 73 | 74 | """ZHA switch.""" 75 | 76 | _domain = DOMAIN 77 | 78 | def __init__(self, **kwargs): 79 | super().__init__(**kwargs) 80 | in_clusters = kwargs['in_clusters'] 81 | out_clusters = kwargs['out_clusters'] 82 | endpoint = kwargs['endpoint'] 83 | # self._groups = None 84 | self._assumed = False 85 | if Groups.cluster_id in self._in_clusters: 86 | self._groups = [] 87 | self._device_state_attributes["Group_id"] = self._groups 88 | clusters = list(out_clusters.items()) + list(in_clusters.items()) 89 | _LOGGER.debug("[0x%04x:%s] initialize cluster listeners: -%s- ", 90 | endpoint._device.nwk, 91 | endpoint.endpoint_id, 92 | clusters) 93 | for (key, cluster) in clusters: 94 | if OnOff.cluster_id == cluster.cluster_id: 95 | self.sub_listener[cluster.cluster_id] = Server_OnOff( 96 | self, cluster, 'OnOff') 97 | elif Scenes.cluster_id == cluster.cluster_id: 98 | self.sub_listener[cluster.cluster_id] = Server_Scenes( 99 | self, cluster, "Scenes") 100 | elif Basic.cluster_id == cluster.cluster_id: 101 | self.sub_listener[cluster.cluster_id] = Server_Basic( 102 | self, cluster, "Basic") 103 | elif Groups.cluster_id == cluster.cluster_id: 104 | self.sub_listener[cluster.cluster_id] = Server_Groups( 105 | self, cluster, "Groups") 106 | else: 107 | self.sub_listener[cluster.cluster_id] = Cluster_Server( 108 | self, cluster, cluster.cluster_id) 109 | 110 | endpoint._device.zdo.add_listener(self) 111 | 112 | @property 113 | def should_poll(self) -> bool: 114 | """Return True if entity has to be polled for state. 115 | False if entity pushes its state to HA. 116 | """ 117 | return True 118 | 119 | @property 120 | def is_on(self) -> bool: 121 | """Return if the switch is on based on the statemachine.""" 122 | if self._state is None: 123 | return False 124 | return bool(self._state) 125 | 126 | def attribute_updated(self, attribute, value): 127 | _LOGGER.debug("attribute update: %s = %s ", attribute, value) 128 | try: 129 | dev_func = self._model.lower().replace(".", "_").replace(" ", "_") 130 | _parse_attribute = getattr(import_module( 131 | "custom_components.device." + dev_func), "_parse_attribute") 132 | (attribute, value) = _parse_attribute( 133 | self, attribute, value, dev_func) 134 | except ImportError as e: 135 | _LOGGER.debug("Import DH %s failed: %s", dev_func, e.args) 136 | except Exception as e: 137 | _LOGGER.info("Excecution of DH %s failed: %s", dev_func, e.args) 138 | if attribute == 0: 139 | self._state = bool(value) 140 | _LOGGER.debug("attribute update: %s = %s ", attribute, value) 141 | self.schedule_update_ha_state() 142 | 143 | async def async_turn_on(self, **kwargs): 144 | """Turn the entity on.""" 145 | await self._endpoint.on_off.on() 146 | self._state = 1 147 | 148 | async def async_turn_off(self, **kwargs): 149 | """Turn the entity off.""" 150 | await self._endpoint.on_off.off() 151 | self._state = 0 152 | 153 | @property 154 | def assumed_state(self) -> bool: 155 | """Return True if unable to access real state of the entity.""" 156 | return bool(self._assumed) 157 | 158 | async def async_update(self): 159 | """Retrieve latest state.""" 160 | _LOGGER.debug("%s async_update", self.entity_id) 161 | if not OnOff.cluster_id in self._in_clusters: 162 | return 163 | try: 164 | result, _ = await self._endpoint.on_off.read_attributes( 165 | ['on_off'], 166 | allow_cache=False, 167 | ) 168 | self._state = result['on_off'] 169 | self._assumed = False 170 | except Exception as e: 171 | self._assumed = True 172 | _LOGGER.debug("%s async_update poll failed: %s", self.entity_id, e) 173 | return True 174 | 175 | if hasattr(self, '_groups'): 176 | try: 177 | result = await self._endpoint.groups.get_membership([]) 178 | except: 179 | result = None 180 | _LOGGER.debug("%s get membership: %s", self.entity_id, result) 181 | if result: 182 | if result[0] >= 1: 183 | self._groups = result[1] 184 | if self._device_state_attributes.get("Group_id") != self._groups: 185 | self._device_state_attributes["Group_id"] = self._groups 186 | for groups in self._groups: 187 | self._endpoint._device._application.listener_event( 188 | 'subscribe_group', 189 | groups) 190 | 191 | def cluster_command(self, tsn, command_id, args): 192 | _LOGGER.debug("cluster command update: %s = %s ", command_id, args) 193 | try: 194 | dev_func = self._model.lower().replace(".", "_").replace(" ", "_") 195 | _custom_cluster_command = getattr( 196 | import_module("custom_components.device." + dev_func), 197 | "_custom_cluster_command" 198 | ) 199 | _custom_cluster_command(self, tsn, command_id, args) 200 | except ImportError as e: 201 | _LOGGER.debug("Import DH %s failed: %s", dev_func, e.args) 202 | except Exception as e: 203 | _LOGGER.info("Excecution of DH %s failed: %s", dev_func, e.args) 204 | 205 | def device_announce(self, *args, **kwargs): 206 | ensure_future(auto_set_attribute_report(self._endpoint, self._in_clusters)) 207 | ensure_future(self.async_update()) 208 | self._assumed = False 209 | _LOGGER.debug("0x%04x device announce for switch received", self._endpoint._device.nwk) 210 | 211 | 212 | async def auto_set_attribute_report(endpoint, in_clusters): 213 | 214 | _LOGGER.debug("[0x%04x:%s] called to set reports", endpoint._device.nwk, endpoint.endpoint_id) 215 | 216 | if 0x0006 in in_clusters: 217 | await zha_new.req_conf_report(endpoint.in_clusters[0x0006], 0, 1, 600, 1) 218 | -------------------------------------------------------------------------------- /zigbee2dot.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | conn = sqlite3.connect('zigbee.db') 3 | c = conn.cursor() 4 | 5 | 6 | header=""" 7 | digraph finite_state_machine { 8 | rankdir=TB; 9 | labeldistance=10; 10 | packMode="node"; 11 | node [shape = doublecircle]; "0"; 12 | node [shape = circle];""" 13 | print(header) 14 | 15 | for row in c.execute("SELECT * from topology INNER JOIN devices ON devices.nwk = topology.src"): 16 | if row[2]: 17 | print("\u0022{}\u0022 [ label = \u0022{}\\n{}\u0022 ];".format(row[0], row[8], row[0])) 18 | print("\u0022{}\u0022 -> \u0022{}\u0022 [ label = \u0022{}/{}\u0022 ]; ".format(row[0], row[1], row[2], row[4])) 19 | print("}") 20 | --------------------------------------------------------------------------------