├── tests ├── __init__.py ├── test_cover.py ├── test_light.py ├── test_sensor.py ├── test_switch.py ├── test_climate.py └── test_binary_sensor.py ├── .mdlrc ├── requirements.txt ├── custom_components └── zigate │ ├── const.py │ ├── manifest.json │ ├── strings.json │ ├── .translations │ ├── en.json │ └── fr.json │ ├── translations │ ├── en.json │ └── fr.json │ ├── config_flow.py │ ├── adminpanel.py │ ├── lock.py │ ├── switch.py │ ├── cover.py │ ├── binary_sensor.py │ ├── sensor.py │ ├── climate.py │ ├── light.py │ ├── services.yaml │ └── __init__.py ├── .gitignore ├── markdownlint.rb ├── hacs.json ├── info.md ├── doc ├── light.zigate.markdown ├── sensor.zigate.markdown ├── switch.zigate.markdown ├── binary_sensor.zigate.markdown └── zigate.markdown ├── LICENSE ├── packages └── zigate.yaml ├── .github └── workflows │ └── python-package.yml └── Readme.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.mdlrc: -------------------------------------------------------------------------------- 1 | rules "~MD041" 2 | style "markdownlint.rb" 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | homeassistant>=0.110 2 | zigate[dev] -------------------------------------------------------------------------------- /custom_components/zigate/const.py: -------------------------------------------------------------------------------- 1 | 2 | DOMAIN = 'zigate' 3 | SCAN_INTERVAL = 120 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.project 2 | /.pydevproject 3 | **/__pycache__ 4 | .history/ 5 | *.code-workspace 6 | -------------------------------------------------------------------------------- /markdownlint.rb: -------------------------------------------------------------------------------- 1 | # Enable all rules by default 2 | all 3 | 4 | rule 'MD013', :line_length => false -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ZiGate", 3 | "domains": ["binary_sensor", "climate", "cover", "light", "sensor", "switch", "lock"], 4 | "iot_class": ["Local Push"] 5 | } -------------------------------------------------------------------------------- /tests/test_cover.py: -------------------------------------------------------------------------------- 1 | """Test zigate cover.""" 2 | import unittest 3 | 4 | 5 | class TestCover(unittest.TestCase): 6 | def test_cover(self): 7 | from custom_components.zigate import cover # noqa 8 | 9 | 10 | if __name__ == '__main__': 11 | unittest.main() 12 | -------------------------------------------------------------------------------- /tests/test_light.py: -------------------------------------------------------------------------------- 1 | """Test zigate light.""" 2 | import unittest 3 | 4 | 5 | class TestLight(unittest.TestCase): 6 | def test_light(self): 7 | from custom_components.zigate import light # noqa 8 | 9 | 10 | if __name__ == '__main__': 11 | unittest.main() 12 | -------------------------------------------------------------------------------- /tests/test_sensor.py: -------------------------------------------------------------------------------- 1 | """Test zigate sensor.""" 2 | import unittest 3 | 4 | 5 | class TestSensor(unittest.TestCase): 6 | def test_sensor(self): 7 | from custom_components.zigate import sensor # noqa 8 | 9 | 10 | if __name__ == '__main__': 11 | unittest.main() 12 | -------------------------------------------------------------------------------- /tests/test_switch.py: -------------------------------------------------------------------------------- 1 | """Test zigate switch.""" 2 | import unittest 3 | 4 | 5 | class TestSwitch(unittest.TestCase): 6 | def test_switch(self): 7 | from custom_components.zigate import switch # noqa 8 | 9 | 10 | if __name__ == '__main__': 11 | unittest.main() 12 | -------------------------------------------------------------------------------- /tests/test_climate.py: -------------------------------------------------------------------------------- 1 | """Test zigate climate.""" 2 | import unittest 3 | 4 | 5 | class TestClimate(unittest.TestCase): 6 | def test_climate(self): 7 | from custom_components.zigate import climate # noqa 8 | 9 | 10 | if __name__ == '__main__': 11 | unittest.main() 12 | -------------------------------------------------------------------------------- /tests/test_binary_sensor.py: -------------------------------------------------------------------------------- 1 | """Test zigate binary sensor.""" 2 | import unittest 3 | 4 | 5 | class TestBinarySensor(unittest.TestCase): 6 | def test_binary_sensor(self): 7 | from custom_components.zigate import binary_sensor # noqa 8 | 9 | 10 | if __name__ == '__main__': 11 | unittest.main() 12 | -------------------------------------------------------------------------------- /info.md: -------------------------------------------------------------------------------- 1 | # Home Assistant ZiGate integration 2 | 3 | A component to use the ZiGate () 4 | 5 | ZiGate is an universal zigbee gateway. 6 | 7 | ## Features 8 | 9 | * Support any zigate (USB, Wifi, PiZiGate, ZiGate DIN). 10 | * Port discovery 11 | * Support sensor, binary sensor, light, switch, climate, cover 12 | -------------------------------------------------------------------------------- /custom_components/zigate/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "zigate", 3 | "name": "ZiGate", 4 | "documentation": "https://github.com/doudz/homeassistant-zigate/blob/master/Readme.md", 5 | "issue_tracker": "https://github.com/doudz/homeassistant-zigate/issues", 6 | "dependencies": ["persistent_notification"], 7 | "codeowners": ["doudz"], 8 | "requirements": ["zigate==0.40.11"], 9 | "version": "20.11.28" 10 | } 11 | -------------------------------------------------------------------------------- /custom_components/zigate/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "title": "ZiGate", 4 | "step": { 5 | "user": { 6 | "title": "ZiGate", 7 | "data": { 8 | "port": "USB Device Path" 9 | } 10 | } 11 | }, 12 | "error": { 13 | "cannot_connect": "Unable to connect to ZiGate." 14 | }, 15 | "abort": { 16 | "single_instance_allowed": "Only a single configuration of ZiGate is allowed." 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /custom_components/zigate/.translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "title": "ZiGate", 4 | "step": { 5 | "user": { 6 | "title": "ZiGate", 7 | "data": { 8 | "port": "USB Device Path" 9 | } 10 | } 11 | }, 12 | "error": { 13 | "cannot_connect": "Unable to connect to ZiGate." 14 | }, 15 | "abort": { 16 | "single_instance_allowed": "Only a single configuration of ZiGate is allowed." 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /custom_components/zigate/.translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "title": "ZiGate", 4 | "step": { 5 | "user": { 6 | "title": "ZiGate", 7 | "data": { 8 | "port": "Port USB" 9 | } 10 | } 11 | }, 12 | "error": { 13 | "cannot_connect": "Impossible de se connecter à ZiGate." 14 | }, 15 | "abort": { 16 | "single_instance_allowed": "Une seule configuration de ZiGate est autorisée." 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /custom_components/zigate/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "title": "ZiGate", 4 | "step": { 5 | "user": { 6 | "title": "ZiGate", 7 | "data": { 8 | "port": "USB Device Path" 9 | } 10 | } 11 | }, 12 | "error": { 13 | "cannot_connect": "Unable to connect to ZiGate." 14 | }, 15 | "abort": { 16 | "single_instance_allowed": "Only a single configuration of ZiGate is allowed." 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /custom_components/zigate/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "title": "ZiGate", 4 | "step": { 5 | "user": { 6 | "title": "ZiGate", 7 | "data": { 8 | "port": "Port USB" 9 | } 10 | } 11 | }, 12 | "error": { 13 | "cannot_connect": "Impossible de se connecter à ZiGate." 14 | }, 15 | "abort": { 16 | "single_instance_allowed": "Une seule configuration de ZiGate est autorisée." 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /doc/light.zigate.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "ZiGate Light" 4 | description: "Instructions on how to integrate ZiGate light into Home Assistant." 5 | date: 2018-07-03 12:00 6 | sidebar: true 7 | comments: false 8 | sharing: true 9 | footer: true 10 | logo: mlight.png 11 | ha_category: Light 12 | ha_iot_class: "Local Push" 13 | --- 14 | 15 | The light platform will be automatically configured if ZiGate component is configured. 16 | 17 | For more configuration information see the [ZiGate component](/components/zigate/) documentation. -------------------------------------------------------------------------------- /doc/sensor.zigate.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "ZiGate Sensor" 4 | description: "Instructions on how to integrate ZiGate sensor into Home Assistant." 5 | date: 2018-07-03 12:00 6 | sidebar: true 7 | comments: false 8 | sharing: true 9 | footer: true 10 | logo: msensor.png 11 | ha_category: Sensor 12 | ha_iot_class: "Local Push" 13 | --- 14 | 15 | The sensor platform will be automatically configured if ZiGate component is configured. 16 | 17 | For more configuration information see the [ZiGate component](/components/zigate/) documentation. -------------------------------------------------------------------------------- /doc/switch.zigate.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "ZiGate Switch" 4 | description: "Instructions on how to integrate ZiGate switch into Home Assistant." 5 | date: 2018-07-03 12:00 6 | sidebar: true 7 | comments: false 8 | sharing: true 9 | footer: true 10 | logo: mswitch.png 11 | ha_category: Switch 12 | ha_iot_class: "Local Push" 13 | --- 14 | 15 | The switch platform will be automatically configured if ZiGate component is configured. 16 | 17 | For more configuration information see the [ZiGate component](/components/zigate/) documentation. -------------------------------------------------------------------------------- /doc/binary_sensor.zigate.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "ZiGate Binary Sensor" 4 | description: "Instructions on how to integrate ZiGate binary sensor into Home Assistant." 5 | date: 2018-07-03 12:00 6 | sidebar: true 7 | comments: false 8 | sharing: true 9 | footer: true 10 | logo: mbinarysensor.png 11 | ha_category: Binary Sensor 12 | ha_iot_class: "Local Push" 13 | --- 14 | 15 | The binary_sensor platform will be automatically configured if ZiGate component is configured. 16 | 17 | For more configuration information see the [ZiGate component](/components/zigate/) documentation. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Sébastien RAMAGE 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /custom_components/zigate/config_flow.py: -------------------------------------------------------------------------------- 1 | """Adds config flow for ZiGate.""" 2 | 3 | from collections import OrderedDict 4 | import voluptuous as vol 5 | from homeassistant import config_entries 6 | from homeassistant.const import CONF_PORT 7 | from .const import DOMAIN 8 | 9 | 10 | @config_entries.HANDLERS.register(DOMAIN) 11 | class ZiGateConfigFlow(config_entries.ConfigFlow): 12 | """ZiGate config flow.""" 13 | async def async_step_user(self, user_input=None): 14 | if self._async_current_entries(): 15 | return self.async_abort(reason="single_instance_allowed") 16 | if self.hass.data.get(DOMAIN): 17 | return self.async_abort(reason="single_instance_allowed") 18 | 19 | errors = {} 20 | 21 | fields = OrderedDict() 22 | fields[vol.Optional(CONF_PORT)] = str 23 | 24 | if user_input is not None: 25 | print(user_input) 26 | return self.async_create_entry(title=user_input.get(CONF_PORT, 'Auto'), data=user_input) 27 | 28 | return self.async_show_form( 29 | step_id="user", data_schema=vol.Schema(fields), errors=errors 30 | ) 31 | 32 | async def async_step_import(self, import_info): 33 | """Handle a ZiGate config import.""" 34 | if self._async_current_entries(): 35 | return self.async_abort(reason="single_instance_allowed") 36 | print('import ', import_info) 37 | 38 | return self.async_create_entry(title="configuration.yaml", data={}) 39 | -------------------------------------------------------------------------------- /packages/zigate.yaml: -------------------------------------------------------------------------------- 1 | 2 | homeassistant: 3 | customize: 4 | ################################################ 5 | ## Node Anchors 6 | ################################################ 7 | package.node_anchors: 8 | customize: &customize 9 | package: 'zigate' 10 | 11 | ################################################ 12 | ## Group 13 | ################################################ 14 | group.zigate_view: 15 | <<: *customize 16 | friendly_name: "ZiGate" 17 | icon: mdi:zigbee 18 | 19 | group.zigate_command: 20 | <<: *customize 21 | friendly_name: "ZiGate Commands" 22 | 23 | group.all_zigate: 24 | <<: *customize 25 | friendly_name: "ZiGate" 26 | hidden: false 27 | 28 | group: 29 | zigate_view: 30 | entities: 31 | - group.all_zigate 32 | - group.zigate_command 33 | 34 | zigate_command: 35 | entities: 36 | - input_boolean.zigate_permit_join 37 | 38 | zigate_entities: 39 | 40 | input_boolean: 41 | zigate_permit_join: 42 | name: Permit Join 43 | 44 | automation: 45 | - alias: zigate_permit_join 46 | trigger: 47 | - entity_id: input_boolean.zigate_permit_join 48 | from: 'off' 49 | platform: state 50 | to: 'on' 51 | condition: [] 52 | action: 53 | - service: zigate.permit_join 54 | - delay: '30' 55 | - data: 56 | entity_id: input_boolean.zigate_permit_join 57 | service: input_boolean.turn_off 58 | 59 | -------------------------------------------------------------------------------- /doc/zigate.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "ZiGate component" 4 | description: "Instructions on how to use the Component ZiGate with Home Assistant." 5 | date: 2018-07-03 12:00 6 | sidebar: true 7 | comments: false 8 | sharing: true 9 | footer: true 10 | logo: home-assistant.png 11 | ha_category: Other 12 | --- 13 | 14 | The `zigate` component allows you to use the ZiGate module () 15 | 16 | Available ZiGate platforms: 17 | 18 | - [Binary sensor](/components/binary_sensor/) (`binary_sensor`) 19 | - [Light](/components/light/) (`light`) 20 | - [Sensor](/components/sensor/) (`sensor`) 21 | - [Switch](/components/switch/) (`switch`) 22 | - [Cover](/components/cover/) (`cover`) 23 | 24 | To integrate the ZiGate component in Home Assistant, add the following section to your `configuration.yaml` file: 25 | 26 | ```yaml 27 | # Example configuration.yaml entry (port auto-discovery) 28 | [zigate]: 29 | 30 | # or 31 | 32 | # Example configuration.yaml entry setting the USB port 33 | [zigate]: 34 | port: /dev/ttyUSB0 35 | channel: 15 36 | enable_led: false 37 | ``` 38 | 39 | or 40 | if you want to use Wifi ZiGate (or usb zigate forwarded with ser2net for example) 41 | Port is optional, default is 9999 42 | 43 | ```yaml 44 | # Enable ZiGate Wifi 45 | zigate: 46 | host: 192.168.0.10:9999 47 | 48 | ``` 49 | 50 | To pair a new device, go in developer/services and call the 'zigate.permit\_join' service. 51 | You have 30 seconds to pair your device. 52 | Entities (sensor, switch, light, etc) will be auto-generated. 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Tests 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: [3.7, 3.8] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install flake8 pytest 30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 31 | - name: Lint with flake8 32 | run: | 33 | # stop the build if there are Python syntax errors or undefined names 34 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 35 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 36 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 37 | - name: Test with pytest 38 | run: | 39 | pytest 40 | 41 | markdownlint: 42 | 43 | runs-on: ubuntu-latest 44 | name: Test Markdown 45 | steps: 46 | - name: Run mdl 47 | uses: actionshub/markdownlint@master 48 | -------------------------------------------------------------------------------- /custom_components/zigate/adminpanel.py: -------------------------------------------------------------------------------- 1 | """ZiGate Admin panel proxy.""" 2 | import aiohttp 3 | from homeassistant.components.http import HomeAssistantView 4 | 5 | 6 | class PanelProxy(HomeAssistantView): 7 | """Reverse Proxy View.""" 8 | 9 | requires_auth = True 10 | cors_allowed = True 11 | name = "panelproxy" 12 | 13 | def __init__(self, url, proxy_url): 14 | """Initialize view url.""" 15 | self.url = url + r"{requested_url:.*}" 16 | self.proxy_url = proxy_url 17 | 18 | async def get(self, request, requested_url): 19 | """Handle GET proxy requests.""" 20 | return await self._handle_request("GET", request, requested_url) 21 | 22 | async def post(self, request, requested_url): 23 | """Handle POST proxy requests.""" 24 | return await self._handle_request("POST", request, requested_url) 25 | 26 | async def _handle_request(self, method, request, requested_url): 27 | """Handle proxy requests.""" 28 | requested_url = requested_url or "/" 29 | headers = request.headers.copy() 30 | headers["Host"] = request.host 31 | headers["X-Real-Ip"] = request.remote 32 | headers["X-Forwarded-For"] = request.remote 33 | headers["X-Forwarded-Proto"] = request.scheme 34 | post_data = await request.read() 35 | async with aiohttp.request( 36 | method, 37 | self.proxy_url + requested_url, 38 | params=request.query, 39 | data=post_data, 40 | headers=headers, 41 | ) as resp: 42 | content = await resp.read() 43 | headers = resp.headers.copy() 44 | return aiohttp.web.Response( 45 | body=content, status=resp.status, headers=headers 46 | ) 47 | 48 | 49 | def adminpanel_setup(hass, url_path): 50 | """Set up the proxy frontend panels.""" 51 | hass.http.register_view(PanelProxy("/" + url_path, 'http://localhost:9998/' + url_path)) 52 | hass.components.frontend.async_register_built_in_panel( 53 | "iframe", 54 | "Zigate Admin", 55 | "mdi:zigbee", 56 | "proxy_" + url_path, 57 | {"url": "/" + url_path}, 58 | require_admin=True, 59 | ) 60 | -------------------------------------------------------------------------------- /custom_components/zigate/lock.py: -------------------------------------------------------------------------------- 1 | """ 2 | ZiGate lock platform that implements locks. 3 | 4 | For more details about this platform, please refer to the documentation 5 | https://home-assistant.io/components/lock.zigate/ 6 | """ 7 | import logging 8 | from homeassistant.exceptions import PlatformNotReady 9 | from homeassistant.components.lock import LockEntity, ENTITY_ID_FORMAT 10 | import zigate 11 | from . import DOMAIN as ZIGATE_DOMAIN 12 | from . import DATA_ZIGATE_ATTRS 13 | 14 | _LOGGER = logging.getLogger(__name__) 15 | 16 | def setup_platform(hass, config, add_devices, discovery_info=None): 17 | """Set up the ZiGate locks.""" 18 | if discovery_info is None: 19 | return 20 | 21 | myzigate = hass.data[ZIGATE_DOMAIN] 22 | 23 | def sync_attributes(): 24 | devs = [] 25 | for device in myzigate.devices: 26 | ieee = device.ieee or device.addr # compatibility 27 | actions = device.available_actions() 28 | if not any(actions.values()): 29 | continue 30 | for endpoint, action_type in actions.items(): 31 | if [zigate.ACTIONS_LOCK] == action_type: 32 | key = '{}-{}-{}'.format(ieee, 33 | 'lock', 34 | endpoint 35 | ) 36 | if key in hass.data[DATA_ZIGATE_ATTRS]: 37 | continue 38 | _LOGGER.debug(('Creating lock ' 39 | 'for device ' 40 | '{} {}').format(device, 41 | endpoint)) 42 | entity = ZiGateLock(hass, device, endpoint) 43 | devs.append(entity) 44 | hass.data[DATA_ZIGATE_ATTRS][key] = entity 45 | 46 | add_devices(devs) 47 | sync_attributes() 48 | zigate.dispatcher.connect(sync_attributes, 49 | zigate.ZIGATE_ATTRIBUTE_ADDED, weak=False) 50 | 51 | 52 | class ZiGateLock(LockEntity): 53 | """Representation of a ZiGate lock.""" 54 | 55 | def __init__(self, hass, device, endpoint): 56 | """Initialize the ZiGate lock.""" 57 | self._device = device 58 | self._endpoint = endpoint 59 | self._is_locked = 2 60 | a = self._device.get_attribute(endpoint, 0x0101, 0) 61 | if a: 62 | self._lock_state = a.get('value', 2) 63 | ieee = device.ieee or device.addr # compatibility 64 | entity_id = 'zigate_{}_{}'.format(ieee, 65 | endpoint) 66 | self.entity_id = ENTITY_ID_FORMAT.format(entity_id) 67 | hass.bus.listen('zigate.attribute_updated', self._handle_event) 68 | 69 | def _handle_event(self, call): 70 | if ( 71 | self._device.ieee == call.data['ieee'] 72 | and self._endpoint == call.data['endpoint'] 73 | ): 74 | _LOGGER.debug("Event received: %s", call.data) 75 | if call.data['cluster'] == 0x0101 and call.data['attribute'] == 0: 76 | self._lock_state = call.data['value'] 77 | if not self.hass: 78 | raise PlatformNotReady 79 | self.schedule_update_ha_state() 80 | 81 | @property 82 | def unique_id(self) -> str: 83 | if self._device.ieee: 84 | return '{}-{}-{}'.format(self._device.ieee, 85 | 'lock', 86 | self._endpoint) 87 | 88 | @property 89 | def should_poll(self): 90 | """No polling needed for a ZiGate lock.""" 91 | return False 92 | 93 | def update(self): 94 | self._device.refresh_device() 95 | 96 | @property 97 | def is_locked(self): 98 | """Return true if lock is locked.""" 99 | return self._lock_state == 1 100 | 101 | def lock(self, **kwargs): 102 | """Lock the device.""" 103 | self._lock_state = 1 104 | self.schedule_update_ha_state() 105 | self.hass.data[ZIGATE_DOMAIN].action_lock(self._device.addr, 106 | self._endpoint, 107 | 0) 108 | 109 | def unlock(self, **kwargs): 110 | """Unlock the device.""" 111 | self._lock_state = 2 112 | self.schedule_update_ha_state() 113 | self.hass.data[ZIGATE_DOMAIN].action_lock(self._device.addr, 114 | self._endpoint, 115 | 1) 116 | -------------------------------------------------------------------------------- /custom_components/zigate/switch.py: -------------------------------------------------------------------------------- 1 | """ 2 | ZiGate platform. 3 | 4 | For more details about this platform, please refer to the documentation 5 | https://home-assistant.io/components/switch.zigate/ 6 | """ 7 | import logging 8 | 9 | from homeassistant.exceptions import PlatformNotReady 10 | from homeassistant.components.switch import SwitchEntity, ENTITY_ID_FORMAT 11 | import zigate 12 | from . import DOMAIN as ZIGATE_DOMAIN 13 | from . import DATA_ZIGATE_ATTRS 14 | 15 | _LOGGER = logging.getLogger(__name__) 16 | 17 | 18 | def setup_platform(hass, config, add_devices, discovery_info=None): 19 | """Set up the ZiGate sensors.""" 20 | if discovery_info is None: 21 | return 22 | 23 | myzigate = hass.data[ZIGATE_DOMAIN] 24 | 25 | def sync_attributes(): 26 | devs = [] 27 | for device in myzigate.devices: 28 | ieee = device.ieee or device.addr # compatibility 29 | actions = device.available_actions() 30 | if not any(actions.values()): 31 | continue 32 | for endpoint, action_type in actions.items(): 33 | if [zigate.ACTIONS_ONOFF] == action_type: 34 | key = '{}-{}-{}'.format(ieee, 35 | 'switch', 36 | endpoint 37 | ) 38 | if key in hass.data[DATA_ZIGATE_ATTRS]: 39 | continue 40 | _LOGGER.debug(('Creating switch ' 41 | 'for device ' 42 | '{} {}').format(device, 43 | endpoint)) 44 | entity = ZiGateSwitch(hass, device, endpoint) 45 | devs.append(entity) 46 | hass.data[DATA_ZIGATE_ATTRS][key] = entity 47 | 48 | add_devices(devs) 49 | sync_attributes() 50 | zigate.dispatcher.connect(sync_attributes, 51 | zigate.ZIGATE_ATTRIBUTE_ADDED, weak=False) 52 | 53 | 54 | class ZiGateSwitch(SwitchEntity): 55 | """Representation of a ZiGate switch.""" 56 | 57 | def __init__(self, hass, device, endpoint): 58 | """Initialize the ZiGate switch.""" 59 | self._device = device 60 | self._endpoint = endpoint 61 | self._is_on = False 62 | a = self._device.get_attribute(endpoint, 6, 0) 63 | if a: 64 | self._is_on = a.get('value', False) 65 | ieee = device.ieee or device.addr # compatibility 66 | entity_id = 'zigate_{}_{}'.format(ieee, 67 | endpoint) 68 | self.entity_id = ENTITY_ID_FORMAT.format(entity_id) 69 | hass.bus.listen('zigate.attribute_updated', self._handle_event) 70 | 71 | def _handle_event(self, call): 72 | if ( 73 | self._device.ieee == call.data['ieee'] 74 | and self._endpoint == call.data['endpoint'] 75 | ): 76 | _LOGGER.debug("Event received: %s", call.data) 77 | if call.data['cluster'] == 6 and call.data['attribute'] == 0: 78 | self._is_on = call.data['value'] 79 | if not self.hass: 80 | raise PlatformNotReady 81 | self.schedule_update_ha_state() 82 | 83 | @property 84 | def unique_id(self) -> str: 85 | if self._device.ieee: 86 | return '{}-{}-{}'.format(self._device.ieee, 87 | 'switch', 88 | self._endpoint) 89 | 90 | @property 91 | def should_poll(self): 92 | """No polling needed for a ZiGate switch.""" 93 | return False 94 | 95 | def update(self): 96 | self._device.refresh_device() 97 | 98 | @property 99 | def name(self): 100 | """Return the name of the device if any.""" 101 | return '{} {}'.format(self._device, 102 | self._endpoint) 103 | 104 | @property 105 | def is_on(self): 106 | """Return true if switch is on.""" 107 | return self._is_on 108 | 109 | def turn_on(self, **kwargs): 110 | """Turn the switch on.""" 111 | self._is_on = True 112 | self.schedule_update_ha_state() 113 | self.hass.data[ZIGATE_DOMAIN].action_onoff(self._device.addr, 114 | self._endpoint, 115 | 1) 116 | 117 | def turn_off(self, **kwargs): 118 | """Turn the device off.""" 119 | self._is_on = False 120 | self.schedule_update_ha_state() 121 | self.hass.data[ZIGATE_DOMAIN].action_onoff(self._device.addr, 122 | self._endpoint, 123 | 0) 124 | 125 | def toggle(self, **kwargs): 126 | """Toggle the device""" 127 | self._is_on = not self._is_on 128 | self.schedule_update_ha_state() 129 | self.hass.data[ZIGATE_DOMAIN].action_onoff(self._device.addr, 130 | self._endpoint, 131 | 2) 132 | 133 | @property 134 | def device_state_attributes(self): 135 | """Return the state attributes.""" 136 | return { 137 | 'addr': self._device.addr, 138 | 'ieee': self._device.ieee, 139 | 'endpoint': '0x{:02x}'.format(self._endpoint), 140 | } 141 | 142 | # @property 143 | # def assumed_state(self)->bool: 144 | # return self._device.assumed_state 145 | -------------------------------------------------------------------------------- /custom_components/zigate/cover.py: -------------------------------------------------------------------------------- 1 | """ 2 | ZiGate cover platform that implements covers. 3 | 4 | For more details about this platform, please refer to the documentation 5 | https://home-assistant.io/components/cover.zigate/ 6 | """ 7 | import logging 8 | 9 | from homeassistant.exceptions import PlatformNotReady 10 | from homeassistant.components.cover import ( 11 | CoverEntity, ENTITY_ID_FORMAT, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP) 12 | import zigate 13 | from . import DOMAIN as ZIGATE_DOMAIN 14 | from . import DATA_ZIGATE_ATTRS 15 | 16 | _LOGGER = logging.getLogger(__name__) 17 | 18 | 19 | def setup_platform(hass, config, add_devices, discovery_info=None): 20 | """Set up the ZiGate sensors.""" 21 | if discovery_info is None: 22 | return 23 | 24 | myzigate = hass.data[ZIGATE_DOMAIN] 25 | 26 | def sync_attributes(): 27 | devs = [] 28 | for device in myzigate.devices: 29 | ieee = device.ieee or device.addr # compatibility 30 | actions = device.available_actions() 31 | if not any(actions.values()): 32 | continue 33 | for endpoint, action_type in actions.items(): 34 | if [zigate.ACTIONS_COVER] == action_type: 35 | key = '{}-{}-{}'.format(ieee, 36 | 'cover', 37 | endpoint 38 | ) 39 | if key in hass.data[DATA_ZIGATE_ATTRS]: 40 | continue 41 | _LOGGER.debug(('Creating cover ' 42 | 'for device ' 43 | '{} {}').format(device, 44 | endpoint)) 45 | entity = ZiGateCover(hass, device, endpoint) 46 | devs.append(entity) 47 | hass.data[DATA_ZIGATE_ATTRS][key] = entity 48 | 49 | add_devices(devs) 50 | sync_attributes() 51 | zigate.dispatcher.connect(sync_attributes, 52 | zigate.ZIGATE_ATTRIBUTE_ADDED, weak=False) 53 | 54 | 55 | class ZiGateCover(CoverEntity): 56 | """Representation of a ZiGate cover.""" 57 | 58 | def __init__(self, hass, device, endpoint): 59 | """Initialize the cover.""" 60 | self._device = device 61 | self._endpoint = endpoint 62 | ieee = device.ieee or device.addr # compatibility 63 | entity_id = 'zigate_{}_{}'.format(ieee, 64 | endpoint) 65 | self.entity_id = ENTITY_ID_FORMAT.format(entity_id) 66 | self._pos = 100 67 | self._available = True 68 | hass.bus.listen('zigate.attribute_updated', self._handle_event) 69 | 70 | def _handle_event(self, call): 71 | if self._device.ieee == call.data['ieee'] and self._endpoint == call.data['endpoint']: 72 | _LOGGER.debug("Attribute update received: %s", call.data) 73 | if call.data['cluster'] == 0x0102 and call.data['attribute'] == 8: 74 | self._pos = call.data['value'] 75 | if not self.hass: 76 | raise PlatformNotReady 77 | self.schedule_update_ha_state() 78 | 79 | @property 80 | def should_poll(self) -> bool: 81 | return False 82 | 83 | def update(self): 84 | self._device.refresh_device() 85 | 86 | @property 87 | def name(self) -> str: 88 | """Return the name of the cover if any.""" 89 | return '{} {}'.format(self._device, 90 | self._endpoint) 91 | 92 | @property 93 | def unique_id(self): 94 | """Return unique ID for cover.""" 95 | if self._device.ieee: 96 | return '{}-{}-{}'.format(self._device.ieee, 97 | 'cover', 98 | self._endpoint) 99 | 100 | @property 101 | def device_state_attributes(self): 102 | """Return the state attributes.""" 103 | return { 104 | 'addr': self._device.addr, 105 | 'ieee': self._device.ieee, 106 | 'endpoint': '0x{:02x}'.format(self._endpoint), 107 | } 108 | 109 | def open_cover(self, **kwargs): 110 | self.hass.data[ZIGATE_DOMAIN].action_cover(self._device.addr, 111 | self._endpoint, 112 | 0x00) 113 | 114 | def close_cover(self, **kwargs): 115 | self.hass.data[ZIGATE_DOMAIN].action_cover(self._device.addr, 116 | self._endpoint, 117 | 0x01) 118 | 119 | def stop_cover(self, **kwargs): 120 | self.hass.data[ZIGATE_DOMAIN].action_cover(self._device.addr, 121 | self._endpoint, 122 | 0x02) 123 | 124 | @property 125 | def current_cover_position(self): 126 | """Return the current position of the cover.""" 127 | _LOGGER.debug("current_cover_position") 128 | attribute = self._device.get_attribute(self._endpoint, 0x0102, 0x0008) 129 | _LOGGER.debug("attribute: %s", attribute) 130 | if attribute: 131 | self._pos = attribute.get('value', 100) 132 | return self._pos 133 | 134 | @property 135 | def supported_features(self): 136 | """Flag supported features.""" 137 | _LOGGER.debug("supported_features") 138 | return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP 139 | 140 | @property 141 | def available(self): 142 | """Return True if entity is available.""" 143 | _LOGGER.debug("available") 144 | return self._available 145 | 146 | @property 147 | def is_closed(self): 148 | """Return if the cover is closed.""" 149 | _LOGGER.debug("is_closed: %s", self._pos) 150 | return self._pos == 0 151 | -------------------------------------------------------------------------------- /custom_components/zigate/binary_sensor.py: -------------------------------------------------------------------------------- 1 | """ 2 | ZiGate platform. 3 | 4 | For more details about this platform, please refer to the documentation 5 | https://home-assistant.io/components/binary_sensor.zigate/ 6 | """ 7 | import logging 8 | 9 | from homeassistant.exceptions import PlatformNotReady 10 | from homeassistant.components.binary_sensor import (BinarySensorEntity, 11 | ENTITY_ID_FORMAT) 12 | import zigate 13 | from . import DOMAIN as ZIGATE_DOMAIN 14 | from . import DATA_ZIGATE_ATTRS 15 | 16 | _LOGGER = logging.getLogger(__name__) 17 | 18 | 19 | def setup_platform(hass, config, add_devices, discovery_info=None): 20 | """Set up the ZiGate sensors.""" 21 | if discovery_info is None: 22 | return 23 | 24 | myzigate = hass.data[ZIGATE_DOMAIN] 25 | 26 | def sync_attributes(): 27 | devs = [] 28 | for device in myzigate.devices: 29 | ieee = device.ieee or device.addr # compatibility 30 | actions = device.available_actions() 31 | if any(actions.values()): 32 | continue 33 | for attribute in device.attributes: 34 | if attribute['cluster'] < 5: 35 | continue 36 | if 'name' in attribute: 37 | key = '{}-{}-{}-{}'.format(ieee, 38 | attribute['endpoint'], 39 | attribute['cluster'], 40 | attribute['attribute'], 41 | ) 42 | value = attribute.get('value') 43 | if value is None: 44 | continue 45 | if key in hass.data[DATA_ZIGATE_ATTRS]: 46 | continue 47 | if type(value) in (bool, dict): 48 | _LOGGER.debug(('Creating binary sensor ' 49 | 'for device ' 50 | '{} {}').format(device, 51 | attribute)) 52 | entity = ZiGateBinarySensor(hass, device, attribute) 53 | devs.append(entity) 54 | hass.data[DATA_ZIGATE_ATTRS][key] = entity 55 | 56 | add_devices(devs) 57 | sync_attributes() 58 | zigate.dispatcher.connect(sync_attributes, 59 | zigate.ZIGATE_ATTRIBUTE_ADDED, weak=False) 60 | 61 | 62 | class ZiGateBinarySensor(BinarySensorEntity): 63 | """representation of a ZiGate binary sensor.""" 64 | 65 | def __init__(self, hass, device, attribute): 66 | """Initialize the sensor.""" 67 | self._device = device 68 | self._attribute = attribute 69 | self._device_class = None 70 | if self._is_zone_status(): 71 | self._is_on = attribute.get('value', {}).get('alarm1', False) 72 | else: 73 | self._is_on = attribute.get('value', False) 74 | name = attribute.get('name') 75 | ieee = device.ieee or device.addr # compatibility 76 | entity_id = 'zigate_{}_{}'.format(ieee, 77 | name) 78 | self.entity_id = ENTITY_ID_FORMAT.format(entity_id) 79 | 80 | typ = self._device.get_value('type', '').lower() 81 | if name == 'presence': 82 | self._device_class = 'motion' 83 | elif 'magnet' in typ: 84 | self._device_class = 'door' 85 | elif 'smok' in typ: 86 | self._device_class = 'smoke' 87 | elif 'zone_status' in name: 88 | self._device_class = 'safety' 89 | hass.bus.listen('zigate.attribute_updated', self._handle_event) 90 | 91 | def _handle_event(self, call): 92 | if ( 93 | self._device.ieee == call.data['ieee'] 94 | and self._attribute['endpoint'] == call.data['endpoint'] 95 | and self._attribute['cluster'] == call.data['cluster'] 96 | and self._attribute['attribute'] == call.data['attribute'] 97 | ): 98 | _LOGGER.debug("Event received: %s", call.data) 99 | if self._is_zone_status(): 100 | self._is_on = call.data['value'].get('alarm1', False) 101 | else: 102 | self._is_on = call.data['value'] 103 | if not self.hass: 104 | raise PlatformNotReady 105 | self.schedule_update_ha_state() 106 | 107 | @property 108 | def device_class(self): 109 | return self._device_class 110 | 111 | @property 112 | def unique_id(self) -> str: 113 | if self._device.ieee: 114 | return '{}-{}-{}-{}'.format(self._device.ieee, 115 | self._attribute['endpoint'], 116 | self._attribute['cluster'], 117 | self._attribute['attribute'], 118 | ) 119 | 120 | @property 121 | def should_poll(self): 122 | """No polling needed for a ZiGate binary sensor.""" 123 | return False 124 | 125 | @property 126 | def name(self): 127 | """Return the name of the binary sensor.""" 128 | return '{} {}'.format(self._attribute.get('name'), 129 | self._device) 130 | 131 | @property 132 | def is_on(self): 133 | """Return true if the binary sensor is on.""" 134 | return self._is_on 135 | 136 | @property 137 | def device_state_attributes(self): 138 | """Return the state attributes.""" 139 | attrs = { 140 | 'addr': self._device.addr, 141 | 'ieee': self._device.ieee, 142 | 'endpoint': '0x{:02x}'.format(self._attribute['endpoint']), 143 | 'cluster': '0x{:04x}'.format(self._attribute['cluster']), 144 | 'attribute': '0x{:04x}'.format(self._attribute['attribute']) 145 | } 146 | if self._is_zone_status(): 147 | attrs.update(self._attribute.get('value')) 148 | return attrs 149 | 150 | def _is_zone_status(self): 151 | '''return True if attribute is a zone status''' 152 | return 'zone_status' in self._attribute.get('name') 153 | -------------------------------------------------------------------------------- /custom_components/zigate/sensor.py: -------------------------------------------------------------------------------- 1 | """ 2 | ZiGate platform. 3 | 4 | For more details about this platform, please refer to the documentation 5 | https://home-assistant.io/components/sensor.zigate/ 6 | """ 7 | import logging 8 | 9 | from homeassistant.exceptions import PlatformNotReady 10 | from homeassistant.components.sensor import ENTITY_ID_FORMAT 11 | from homeassistant.const import (DEVICE_CLASS_HUMIDITY, 12 | DEVICE_CLASS_TEMPERATURE, 13 | DEVICE_CLASS_ILLUMINANCE, 14 | DEVICE_CLASS_PRESSURE, 15 | STATE_UNAVAILABLE) 16 | from homeassistant.helpers.entity import Entity 17 | import zigate 18 | from . import DOMAIN as ZIGATE_DOMAIN 19 | from . import DATA_ZIGATE_ATTRS 20 | 21 | _LOGGER = logging.getLogger(__name__) 22 | 23 | 24 | def setup_platform(hass, config, add_devices, discovery_info=None): 25 | """Set up the ZiGate sensors.""" 26 | if discovery_info is None: 27 | return 28 | 29 | myzigate = hass.data[ZIGATE_DOMAIN] 30 | 31 | def sync_attributes(**kwargs): 32 | devs = [] 33 | for device in myzigate.devices: 34 | ieee = device.ieee or device.addr # compatibility 35 | actions = device.available_actions() 36 | if any(actions.values()): 37 | continue 38 | for attribute in device.attributes: 39 | if attribute['cluster'] < 5: 40 | continue 41 | if 'name' in attribute: 42 | key = '{}-{}-{}-{}'.format(ieee, 43 | attribute['endpoint'], 44 | attribute['cluster'], 45 | attribute['attribute'], 46 | ) 47 | value = attribute.get('value') 48 | if value is None: 49 | continue 50 | if key in hass.data[DATA_ZIGATE_ATTRS]: 51 | continue 52 | if type(value) not in (bool, dict): 53 | _LOGGER.debug(('Creating sensor ' 54 | 'for device ' 55 | '{} {}').format(device, 56 | attribute)) 57 | entity = ZiGateSensor(hass, device, attribute) 58 | devs.append(entity) 59 | hass.data[DATA_ZIGATE_ATTRS][key] = entity 60 | 61 | add_devices(devs) 62 | 63 | sync_attributes() 64 | zigate.dispatcher.connect(sync_attributes, 65 | zigate.ZIGATE_ATTRIBUTE_ADDED, weak=False) 66 | 67 | 68 | class ZiGateSensor(Entity): 69 | """Representation of a ZiGate sensor.""" 70 | 71 | def __init__(self, hass, device, attribute): 72 | """Initialize the sensor.""" 73 | self._device = device 74 | self._attribute = attribute 75 | self._device_class = None 76 | self._state = attribute.get('value', STATE_UNAVAILABLE) 77 | name = attribute.get('name') 78 | ieee = device.ieee or device.addr # compatibility 79 | entity_id = 'zigate_{}_{}'.format(ieee, 80 | name) 81 | self.entity_id = ENTITY_ID_FORMAT.format(entity_id) 82 | 83 | if 'temperature' in name: 84 | self._device_class = DEVICE_CLASS_TEMPERATURE 85 | elif 'humidity' in name: 86 | self._device_class = DEVICE_CLASS_HUMIDITY 87 | elif 'luminosity' in name: 88 | self._device_class = DEVICE_CLASS_ILLUMINANCE 89 | elif 'pressure' in name: 90 | self._device_class = DEVICE_CLASS_PRESSURE 91 | hass.bus.listen('zigate.attribute_updated', self._handle_event) 92 | 93 | def _handle_event(self, call): 94 | if ( 95 | self._device.ieee == call.data['ieee'] 96 | and self._attribute['endpoint'] == call.data['endpoint'] 97 | and self._attribute['cluster'] == call.data['cluster'] 98 | and self._attribute['attribute'] == call.data['attribute'] 99 | ): 100 | _LOGGER.debug("Event received: %s", call.data) 101 | self._state = call.data['value'] 102 | if not self.hass: 103 | raise PlatformNotReady 104 | self.schedule_update_ha_state() 105 | 106 | @property 107 | def unique_id(self) -> str: 108 | if self._device.ieee: 109 | return '{}-{}-{}-{}'.format(self._device.ieee, 110 | self._attribute['endpoint'], 111 | self._attribute['cluster'], 112 | self._attribute['attribute'], 113 | ) 114 | 115 | @property 116 | def should_poll(self): 117 | """No polling needed for a ZiGate sensor.""" 118 | return False 119 | 120 | @property 121 | def device_class(self): 122 | """Return the device class of the sensor.""" 123 | return self._device_class 124 | 125 | @property 126 | def name(self): 127 | """Return the name of the sensor.""" 128 | return '{} {}'.format(self._attribute.get('name'), 129 | self._device) 130 | 131 | @property 132 | def state(self): 133 | """Return the state of the sensor.""" 134 | return self._state 135 | 136 | @property 137 | def unit_of_measurement(self): 138 | """Return the unit this state is expressed in.""" 139 | return self._attribute.get('unit') 140 | 141 | @property 142 | def device_state_attributes(self): 143 | """Return the state attributes.""" 144 | attrs = { 145 | 'addr': self._device.addr, 146 | 'ieee': self._device.ieee, 147 | 'endpoint': '0x{:02x}'.format(self._attribute['endpoint']), 148 | 'cluster': '0x{:04x}'.format(self._attribute['cluster']), 149 | 'attribute': '0x{:04x}'.format(self._attribute['attribute']) 150 | } 151 | state = self.state 152 | if isinstance(self.state, dict): 153 | attrs.update(state) 154 | return attrs 155 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # ZiGate component for Home Assistant 2 | 3 | *** 4 | # :warning: Since Home Assistant 0.118 this integration is deprecated ! 5 | # I recommand ZHA integration instead with ZiGate firmware 3.1d 6 | more informations : 7 | - https://www.home-assistant.io/integrations/zha/ 8 | - https://github.com/zigpy/zigpy-zigate 9 | *** 10 | 11 | A new component to use the ZiGate () 12 | 13 | ![Tests](https://github.com/doudz/homeassistant-zigate/workflows/Tests/badge.svg) 14 | [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/sebramage) 15 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-green.svg)](https://github.com/custom-components/hacs) 16 | [![Donate with Bitcoin](https://en.cryptobadges.io/badge/small/3DHvPBWyf5Vsp485tGFu7WfYSd6r5qgZdH)](https://en.cryptobadges.io/donate/3DHvPBWyf5Vsp485tGFu7WfYSd6r5qgZdH) 17 | 18 | Currently it supports sensor, binary_sensor and switch, light, cover and climate 19 | 20 | To install: 21 | 22 | - if not exists, create folder 'custom\_components' under your home assitant directory (beside configuration.yaml) 23 | - copy all the files in your hass folder, under 'custom\_components' like that : 24 | 25 | ```text 26 | custom_components/ 27 | └── zigate 28 | ├── __init__.py 29 | ├── services.yaml 30 | ├── switch.py 31 | ├── sensor.py 32 | ├── light.py 33 | ├── binary_sensor.py 34 | ├── cover.py 35 | └── climate.py 36 | ``` 37 | - restart your Home Assistant, so that it installs requirements 38 | - adapt your configuration.yaml 39 | 40 | To pair a new device, go in developer/services and call the 'zigate.permit\_join' service. 41 | You have 30 seconds to pair your device. 42 | 43 | Configuration example : 44 | 45 | ```yaml 46 | # Enable ZiGate (port will be auto-discovered) 47 | zigate: 48 | 49 | ``` 50 | 51 | or 52 | 53 | ```yaml 54 | # Enable ZiGate 55 | zigate: 56 | port: /dev/ttyUSB0 57 | channel: 24 58 | enable_led : false 59 | 60 | ``` 61 | 62 | or 63 | if you want to use Wifi ZiGate (or usb zigate forwarded with ser2net for example) 64 | Port is optional, default is 9999 65 | 66 | ```yaml 67 | # Enable ZiGate Wifi 68 | zigate: 69 | host: 192.168.0.10:9999 70 | 71 | ``` 72 | 73 | If you want to use PiZiGate, just add `gpio: true`. Other options are still available (channel, etc) 74 | 75 | ```yaml 76 | # Enable PiZiGate 77 | zigate: 78 | gpio: true 79 | port: /dev/ttyAMA0 80 | # Regarding the raspberry 81 | #port: /dev/serial0 82 | 83 | ``` 84 | 85 | If you're using Rpi3, you can have some trouble trying to use PiZiGate. 86 | If needed, add the following line into config.txt (If you're using Hass.io you have to access that on the SD card directly. Simply plug it into your PC and edit it there. The config.txt is not accessible from your Hass.io system, you may need to open the SD card on a Windows or Linux system.): 87 | 88 | ```text 89 | dtoverlay=pi3-miniuart-bt 90 | enable_uart=1 91 | ``` 92 | 93 | ## Polling 94 | 95 | By default, the component will poll the state of any device available on idle (light, relay, etc) every 120sec 96 | 97 | To disable polling, add `polling: false` in config 98 | 99 | You can adjust the polling by setting `scan_interval` in config (default to 120) 100 | 101 | ## Upgrade firmware 102 | 103 | You could upgrade the zigate firmware to the latest available release by calling `zigate.upgrade_firmware`. 104 | 105 | - If you're using PiZigate or ZiGate DIN the process is fully automatic 106 | - If you're using USB TTL ZiGate you have to put zigate in download mode first 107 | - Always call zigate.stop_zigate before unplugging the USB ZiGate 108 | 109 | ## Admin Panel 110 | 111 | ZiGate lib has now an embedded admin panel, to enable it, add `admin_panel: true` in config. 112 | To access the admin you need to open another browser to addresse http://[ip_address]:9998 113 | Integration of the admin panel in HA has been disabled because of security issue 114 | 115 | ```yaml 116 | zigate: 117 | admin_panel: true 118 | ``` 119 | 120 | ## Package 121 | 122 | Additionnally you could add the zigate package to have a new tab with all zigate devices and a "permit join" switch. 123 | 124 | To install, just copy the "packages" folder in your hass config folder and if needed add the following in your configuration.yaml 125 | 126 | ```yaml 127 | homeassistant: 128 | packages: !include_dir_named packages 129 | ``` 130 | 131 | ## How enable debug log 132 | 133 | ```yaml 134 | logger: 135 | default: error 136 | logs: 137 | zigate: debug 138 | custom_components.zigate: debug 139 | 140 | ``` 141 | 142 | Alternatively you could call the service `logger.set_level` with data `{"custom_components.zigate": "debug", "zigate": "debug"}` 143 | 144 | ## How to adjust device parameter 145 | 146 | Some devices have the ability to change some parameters, for example on the Xiaomi vibration sensor you can adujst the sensibility. You'll be able to do that using the service `write_attribute` with parameters : 147 | `{ "addr": "8c37", "endpoint":"1", "cluster":"0", "attribute_id":"0xFF0D", "manufacturer_code":"0x115F", "attribute_type":"0x20", "value":"0x01" }` 148 | 149 | In this example, the value is the sensiblity, it could be 0x01 for "high sens", 0x0B for "medium" and 0x15 for "low" 150 | 151 | ## Battery level 152 | 153 | I recommand the following package to create a nice battery tab with alerts ! 154 | [battery_alert.yaml](https://github.com/notoriousbdg/Home-AssistantConfig/blob/master/packages/battery_alert.yaml) 155 | 156 | ## How to contribute 157 | 158 | If you are looking to make a contribution to this project we suggest that you follow the steps in these guides: 159 | 160 | - 161 | - 162 | 163 | Some developers might also be interested in receiving donations in the form of hardware such as Zigbee modules or devices, and even if such donations are most often donated with no strings attached it could in many cases help the developers motivation and indirect improve the development of this project. 164 | 165 | ## Comment contribuer 166 | 167 | Si vous souhaitez apporter une contribution à ce projet, nous vous suggérons de suivre les étapes décrites dans ces guides: 168 | 169 | - 170 | - 171 | 172 | Certains développeurs pourraient également être intéressés par des dons sous forme de matériel, tels que des modules ou des dispositifs Zigbee, et même si ces dons sont le plus souvent donnés sans aucune condition, cela pourrait dans de nombreux cas motiver les développeurs et indirectement améliorer le développement de ce projet. 173 | -------------------------------------------------------------------------------- /custom_components/zigate/climate.py: -------------------------------------------------------------------------------- 1 | """ 2 | ZiGate climate platform that implements climates. 3 | 4 | For more details about this platform, please refer to the documentation 5 | https://home-assistant.io/components/climate.zigate/ 6 | """ 7 | import logging 8 | 9 | from homeassistant.exceptions import PlatformNotReady 10 | from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS 11 | from homeassistant.components.climate import ClimateEntity, ENTITY_ID_FORMAT 12 | from homeassistant.components.climate.const import SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE, HVAC_MODE_HEAT 13 | import zigate 14 | from . import DOMAIN as ZIGATE_DOMAIN 15 | from . import DATA_ZIGATE_ATTRS 16 | SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE 17 | 18 | _LOGGER = logging.getLogger(__name__) 19 | 20 | 21 | def setup_platform(hass, config, add_entities, discovery_info=None): 22 | """Set up the zigate climate devices.""" 23 | if discovery_info is None: 24 | return 25 | 26 | myzigate = hass.data[ZIGATE_DOMAIN] 27 | 28 | def sync_attributes(): 29 | devs = [] 30 | for device in myzigate.devices: 31 | ieee = device.ieee or device.addr # compatibility 32 | actions = device.available_actions() 33 | if not any(actions.values()): 34 | continue 35 | for endpoint, action_type in actions.items(): 36 | if [zigate.ACTIONS_THERMOSTAT] == action_type: 37 | key = '{}-{}-{}'.format(ieee, 38 | 'climate', 39 | endpoint 40 | ) 41 | if key in hass.data[DATA_ZIGATE_ATTRS]: 42 | continue 43 | _LOGGER.debug(('Creating climate ' 44 | 'for device ' 45 | '{} {}').format(device, 46 | endpoint)) 47 | entity = ZigateClimate(hass, device, endpoint) 48 | devs.append(entity) 49 | hass.data[DATA_ZIGATE_ATTRS][key] = entity 50 | 51 | add_entities(devs) 52 | sync_attributes() 53 | zigate.dispatcher.connect(sync_attributes, 54 | zigate.ZIGATE_ATTRIBUTE_ADDED, weak=False) 55 | 56 | 57 | class ZigateClimate(ClimateEntity): 58 | """Representation of a Zigate climate device.""" 59 | 60 | def __init__(self, hass, device, endpoint): 61 | """Initialize the ZiGate climate.""" 62 | self._device = device 63 | self._endpoint = endpoint 64 | ieee = device.ieee or device.addr # compatibility 65 | entity_id = 'zigate_{}_{}'.format(ieee, 66 | endpoint) 67 | self.entity_id = ENTITY_ID_FORMAT.format(entity_id) 68 | hass.bus.listen('zigate.attribute_updated', self._handle_event) 69 | 70 | self._support_flags = SUPPORT_FLAGS 71 | self._hvac_mode = HVAC_MODE_HEAT 72 | 73 | def _handle_event(self, call): 74 | if ( 75 | self._device.ieee == call.data['ieee'] 76 | and self._endpoint == call.data['endpoint'] 77 | ): 78 | _LOGGER.debug("Event received: %s", call.data) 79 | if not self.hass: 80 | raise PlatformNotReady 81 | self.schedule_update_ha_state() 82 | 83 | @property 84 | def unique_id(self) -> str: 85 | if self._device.ieee: 86 | return '{}-{}-{}'.format(self._device.ieee, 87 | 'climate', 88 | self._endpoint) 89 | 90 | @property 91 | def supported_features(self): 92 | """Return the list of supported features.""" 93 | return self._support_flags 94 | 95 | @property 96 | def should_poll(self) -> bool: 97 | return False 98 | 99 | def update(self): 100 | self._device.refresh_device() 101 | 102 | @property 103 | def name(self): 104 | """Return the name of the device if any.""" 105 | return '{} {}'.format(self._device, 106 | self._endpoint) 107 | 108 | @property 109 | def temperature_unit(self): 110 | """Return the unit of measurement.""" 111 | return TEMP_CELSIUS 112 | 113 | @property 114 | def current_temperature(self): 115 | """Return the current temperature.""" 116 | t = 0 117 | a = self._device.get_attribute(self._endpoint, 0x0201, 0x0000) 118 | if a: 119 | t = a.get('value', 0) 120 | return t 121 | 122 | @property 123 | def target_temperature(self): 124 | """Return the temperature we try to reach.""" 125 | if self.preset_mode == 'away': 126 | attr = 0x0014 127 | else: 128 | attr = 0x0012 129 | t = 0 130 | a = self._device.get_attribute(self._endpoint, 0x0201, attr) 131 | if a: 132 | t = a.get('value', 0) 133 | return t 134 | 135 | @property 136 | def hvac_mode(self): 137 | return self._hvac_mode 138 | 139 | @property 140 | def hvac_modes(self): 141 | return [HVAC_MODE_HEAT] 142 | 143 | def set_hvac_mode(self, hvac_mode): 144 | self._hvac_mode = hvac_mode 145 | 146 | @property 147 | def preset_modes(self): 148 | return ['home', 'away'] 149 | 150 | @property 151 | def preset_mode(self): 152 | t = 1 153 | a = self._device.get_attribute(self._endpoint, 0x0201, 0x0002) 154 | if a: 155 | t = a.get('value', t) 156 | if t == 0: 157 | return 'away' 158 | return 'home' 159 | 160 | def set_preset_mode(self, preset_mode: str): 161 | """Set new preset mode.""" 162 | if preset_mode == 'away': 163 | self.hass.data[ZIGATE_DOMAIN].write_attribute_request(self._device.addr, 164 | self._endpoint, 165 | 0x0201, 166 | [(0x0002, 0x18, 0)]) 167 | else: 168 | self.hass.data[ZIGATE_DOMAIN].write_attribute_request(self._device.addr, 169 | self._endpoint, 170 | 0x0201, 171 | [(0x0002, 0x18, 1)]) 172 | 173 | def set_temperature(self, **kwargs): 174 | """Set new target temperatures.""" 175 | if kwargs.get(ATTR_TEMPERATURE) is not None: 176 | temp = int(kwargs.get(ATTR_TEMPERATURE) * 100) 177 | if self.preset_mode == 'away': 178 | attr = 0x0014 179 | else: 180 | attr = 0x0012 181 | self.hass.data[ZIGATE_DOMAIN].write_attribute_request(self._device.addr, 182 | self._endpoint, 183 | 0x0201, 184 | [(attr, 0x29, temp)]) 185 | self.schedule_update_ha_state() 186 | 187 | @property 188 | def device_state_attributes(self): 189 | """Return the state attributes.""" 190 | return { 191 | 'addr': self._device.addr, 192 | 'ieee': self._device.ieee, 193 | 'endpoint': '0x{:02x}'.format(self._endpoint), 194 | } 195 | -------------------------------------------------------------------------------- /custom_components/zigate/light.py: -------------------------------------------------------------------------------- 1 | """ 2 | ZiGate light platform that implements lights. 3 | 4 | For more details about this platform, please refer to the documentation 5 | https://home-assistant.io/components/light.zigate/ 6 | """ 7 | import logging 8 | from functools import reduce 9 | from operator import ior 10 | 11 | from homeassistant.exceptions import PlatformNotReady 12 | import homeassistant.util.color as color_util 13 | from homeassistant.components.light import ( 14 | ATTR_BRIGHTNESS, ATTR_TRANSITION, ATTR_HS_COLOR, 15 | SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, 16 | SUPPORT_TRANSITION, ATTR_COLOR_TEMP, 17 | SUPPORT_COLOR, LightEntity, ENTITY_ID_FORMAT) 18 | import zigate 19 | from . import DOMAIN as ZIGATE_DOMAIN 20 | from . import DATA_ZIGATE_ATTRS 21 | 22 | 23 | _LOGGER = logging.getLogger(__name__) 24 | 25 | SUPPORT_HUE_COLOR = 64 26 | 27 | 28 | def setup_platform(hass, config, add_devices, discovery_info=None): 29 | """Set up the ZiGate sensors.""" 30 | if discovery_info is None: 31 | return 32 | 33 | myzigate = hass.data[ZIGATE_DOMAIN] 34 | LIGHT_ACTIONS = [zigate.ACTIONS_LEVEL, 35 | zigate.ACTIONS_COLOR, 36 | zigate.ACTIONS_TEMPERATURE, 37 | zigate.ACTIONS_HUE, 38 | ] 39 | 40 | def sync_attributes(): 41 | devs = [] 42 | for device in myzigate.devices: 43 | ieee = device.ieee or device.addr # compatibility 44 | actions = device.available_actions() 45 | if not any(actions.values()): 46 | continue 47 | for endpoint, action_type in actions.items(): 48 | if any(i in action_type for i in LIGHT_ACTIONS): 49 | key = '{}-{}-{}'.format(ieee, 50 | 'light', 51 | endpoint 52 | ) 53 | if key in hass.data[DATA_ZIGATE_ATTRS]: 54 | continue 55 | _LOGGER.debug(('Creating light ' 56 | 'for device ' 57 | '{} {}').format(device, 58 | endpoint)) 59 | entity = ZiGateLight(hass, device, endpoint) 60 | devs.append(entity) 61 | hass.data[DATA_ZIGATE_ATTRS][key] = entity 62 | 63 | add_devices(devs) 64 | sync_attributes() 65 | zigate.dispatcher.connect(sync_attributes, 66 | zigate.ZIGATE_ATTRIBUTE_ADDED, weak=False) 67 | 68 | 69 | class ZiGateLight(LightEntity): 70 | """Representation of a ZiGate light.""" 71 | 72 | def __init__(self, hass, device, endpoint): 73 | """Initialize the light.""" 74 | self._device = device 75 | self._endpoint = endpoint 76 | self._is_on = False 77 | self._brightness = 0 78 | a = self._device.get_attribute(endpoint, 6, 0) 79 | if a: 80 | self._is_on = a.get('value', False) 81 | ieee = device.ieee or device.addr # compatibility 82 | entity_id = 'zigate_{}_{}'.format(ieee, 83 | endpoint) 84 | self.entity_id = ENTITY_ID_FORMAT.format(entity_id) 85 | 86 | import zigate 87 | supported_features = set() 88 | supported_features.add(SUPPORT_TRANSITION) 89 | for action_type in device.available_actions(endpoint)[endpoint]: 90 | if action_type == zigate.ACTIONS_LEVEL: 91 | supported_features.add(SUPPORT_BRIGHTNESS) 92 | elif action_type == zigate.ACTIONS_COLOR: 93 | supported_features.add(SUPPORT_COLOR) 94 | elif action_type == zigate.ACTIONS_TEMPERATURE: 95 | supported_features.add(SUPPORT_COLOR_TEMP) 96 | elif action_type == zigate.ACTIONS_HUE: 97 | supported_features.add(SUPPORT_HUE_COLOR) 98 | self._supported_features = reduce(ior, supported_features) 99 | hass.bus.listen('zigate.attribute_updated', self._handle_event) 100 | 101 | def _handle_event(self, call): 102 | if ( 103 | self._device.ieee == call.data['ieee'] 104 | and self._endpoint == call.data['endpoint'] 105 | ): 106 | _LOGGER.debug("Event received: %s", call.data) 107 | if call.data['cluster'] == 6 and call.data['attribute'] == 0: 108 | self._is_on = call.data['value'] 109 | if call.data['cluster'] == 8 and call.data['attribute'] in (0, 17): 110 | self._brightness = int(call.data['value'] * 255 / 100) 111 | if not self.hass: 112 | raise PlatformNotReady 113 | self.schedule_update_ha_state() 114 | 115 | @property 116 | def should_poll(self) -> bool: 117 | return False 118 | 119 | def update(self): 120 | self._device.refresh_device() 121 | 122 | @property 123 | def name(self) -> str: 124 | """Return the name of the light if any.""" 125 | return '{} {}'.format(self._device, 126 | self._endpoint) 127 | 128 | @property 129 | def unique_id(self): 130 | """Return unique ID for light.""" 131 | if self._device.ieee: 132 | return '{}-{}-{}'.format(self._device.ieee, 133 | 'light', 134 | self._endpoint) 135 | 136 | @property 137 | def brightness(self) -> int: 138 | """Return the brightness of this light between 0..255.""" 139 | return self._brightness 140 | 141 | @property 142 | def hs_color(self) -> tuple: 143 | """Return the hs color value.""" 144 | h = 0 145 | a = self._device.get_attribute(self._endpoint, 0x0300, 0x0000) 146 | if a: 147 | h = a.get('value', 0) 148 | s = 0 149 | a = self._device.get_attribute(self._endpoint, 0x0300, 0x0001) 150 | if a: 151 | s = a.get('value', 0) 152 | return (h, s) 153 | 154 | @property 155 | def color_temp(self) -> int: 156 | """Return the CT color temperature.""" 157 | a = self._device.get_attribute(self._endpoint, 0x0300, 0x0007) 158 | if a: 159 | return a.get('value') 160 | 161 | @property 162 | def is_on(self) -> bool: 163 | """Return true if light is on.""" 164 | return self._is_on 165 | 166 | @property 167 | def supported_features(self) -> int: 168 | """Flag supported features.""" 169 | return self._supported_features 170 | 171 | def turn_on(self, **kwargs): 172 | """Turn the switch on.""" 173 | self._is_on = True 174 | self.schedule_update_ha_state() 175 | transition = 1 176 | if ATTR_TRANSITION in kwargs: 177 | transition = int(kwargs[ATTR_TRANSITION]) 178 | if ATTR_BRIGHTNESS in kwargs: 179 | brightness = kwargs[ATTR_BRIGHTNESS] 180 | self._brightness = brightness 181 | brightness = round((brightness / 255) * 100) or 1 182 | self.hass.data[ZIGATE_DOMAIN].action_move_level_onoff(self._device.addr, 183 | self._endpoint, 184 | 1, 185 | brightness, 186 | transition 187 | ) 188 | else: 189 | self.hass.data[ZIGATE_DOMAIN].action_onoff(self._device.addr, 190 | self._endpoint, 191 | 1) 192 | if ATTR_HS_COLOR in kwargs: 193 | h, s = kwargs[ATTR_HS_COLOR] 194 | if self.supported_features & SUPPORT_COLOR: 195 | x, y = color_util.color_hs_to_xy(h, s) 196 | self.hass.data[ZIGATE_DOMAIN].action_move_colour(self._device.addr, 197 | self._endpoint, 198 | x, 199 | y, 200 | transition) 201 | elif self.supported_features & SUPPORT_HUE_COLOR: 202 | self.hass.data[ZIGATE_DOMAIN].action_move_hue_saturation(self._device.addr, 203 | self._endpoint, 204 | int(h), 205 | int(s), 206 | transition) 207 | elif ATTR_COLOR_TEMP in kwargs: 208 | temp = kwargs[ATTR_COLOR_TEMP] 209 | self.hass.data[ZIGATE_DOMAIN].action_move_temperature(self._device.addr, 210 | self._endpoint, 211 | int(temp), 212 | transition) 213 | 214 | def turn_off(self, **kwargs): 215 | """Turn the device off.""" 216 | self._is_on = False 217 | self.schedule_update_ha_state() 218 | self.hass.data[ZIGATE_DOMAIN].action_onoff(self._device.addr, 219 | self._endpoint, 220 | 0) 221 | 222 | def toggle(self, **kwargs): 223 | """Toggle the device""" 224 | self._is_on = not self._is_on 225 | self.schedule_update_ha_state() 226 | self.hass.data[ZIGATE_DOMAIN].action_onoff(self._device.addr, 227 | self._endpoint, 228 | 2) 229 | 230 | @property 231 | def device_state_attributes(self): 232 | """Return the state attributes.""" 233 | return { 234 | 'addr': self._device.addr, 235 | 'ieee': self._device.ieee, 236 | 'endpoint': '0x{:02x}'.format(self._endpoint), 237 | } 238 | -------------------------------------------------------------------------------- /custom_components/zigate/services.yaml: -------------------------------------------------------------------------------- 1 | permit_join: 2 | description: Allow pairing new device. 3 | 4 | refresh_device: 5 | description: > 6 | Refresh a device by sending many requests to it. 7 | Be aware that devices running on battery need to be woken up to answer, 8 | typically by pushing the pairing button. 9 | You should provide the entity_id OR the addr OR the ieee. 10 | If no parameter is provided, every devices will be refreshed. 11 | fields: 12 | entity_id: 13 | description: The device entity_id to refresh 14 | example: 'zigate.0123456789abcdef' 15 | addr: 16 | description: ZiGate address of the device. 17 | example: 'af7d' 18 | ieee: 19 | description: IEEE address of the device. 20 | example: '0123456789abcdef' 21 | 22 | discover_device: 23 | description: > 24 | Discover a device by sending many requests to it. 25 | Be aware that devices running on battery need to be woken up to answer, 26 | typically by pushing the pairing button. 27 | You should provide the entity_id OR the addr OR the ieee. 28 | If no parameter is provided, every devices will be discovered. 29 | fields: 30 | entity_id: 31 | description: The device entity_id to refresh 32 | example: 'zigate.0123456789abcdef' 33 | addr: 34 | description: ZiGate address of the device. 35 | example: 'af7d' 36 | ieee: 37 | description: IEEE address of the device. 38 | example: '0123456789abcdef' 39 | 40 | remove_device: 41 | description: > 42 | Remove a device. 43 | You should provide the entity_id OR the addr OR the ieee. 44 | fields: 45 | entity_id: 46 | description: The device entity_id to remove 47 | example: 'zigate.0123456789abcdef' 48 | addr: 49 | description: ZiGate address of the device. 50 | example: 'af7d' 51 | ieee: 52 | description: IEEE address of the device. 53 | example: '0123456789abcdef' 54 | 55 | identify_device: 56 | description: > 57 | Automatically identify a device's destination endpoint. 58 | Be aware that devices running on battery need to be woken up to answer, 59 | typically by pushing the pairing button. 60 | You should provide the entity_id OR the addr OR the ieee. 61 | fields: 62 | entity_id: 63 | description: The device entity_id to identify 64 | example: 'zigate.0123456789abcdef' 65 | addr: 66 | description: ZiGate address of the device. 67 | example: 'af7d' 68 | ieee: 69 | description: IEEE address of the device. 70 | example: '0123456789abcdef' 71 | 72 | raw_command: 73 | description: Send a raw command to zigate. 74 | fields: 75 | cmd: 76 | description: Command code 77 | example: '0x0092' 78 | data: 79 | description: Payload in hex format. 80 | example: '02af7d010101' 81 | 82 | read_attribute: 83 | description: > 84 | Read attribute from device 85 | You should provide the entity_id OR the addr OR the ieee. 86 | fields: 87 | entity_id: 88 | description: The device entity_id to identify 89 | example: 'zigate.0123456789abcdef' 90 | addr: 91 | description: ZiGate address of the device. 92 | example: 'af7d' 93 | ieee: 94 | description: IEEE address of the device. 95 | example: '0123456789abcdef' 96 | endpoint: 97 | description: Device endpoint. 98 | example: '1' 99 | cluster: 100 | description: Device endpoint cluster. 101 | example: '0' 102 | attribute_id: 103 | description: Attribute ID to read. 104 | example: '0xFF0D' 105 | manufacturer_code: 106 | description: Optional Manufacturer Code. 107 | example: '0x115F' 108 | 109 | write_attribute: 110 | description: > 111 | Read attribute from device 112 | You should provide the entity_id OR the addr OR the ieee. 113 | fields: 114 | entity_id: 115 | description: The device entity_id to identify 116 | example: 'zigate.0123456789abcdef' 117 | addr: 118 | description: ZiGate address of the device. 119 | example: 'af7d' 120 | ieee: 121 | description: IEEE address of the device. 122 | example: '0123456789abcdef' 123 | endpoint: 124 | description: Device endpoint. 125 | example: '1' 126 | cluster: 127 | description: Device endpoint cluster. 128 | example: '0' 129 | attribute_id: 130 | description: Attribute ID to write. 131 | example: '0xFF0D' 132 | value: 133 | description: Value to write 134 | example: '0x01' 135 | attribute_type: 136 | description: Attribute type to write. 137 | example: '0x20' 138 | manufacturer_code: 139 | description: Optional Manufacturer Code. 140 | example: '0x115F' 141 | 142 | add_group: 143 | description: > 144 | Add group to device 145 | fields: 146 | entity_id: 147 | description: The device entity_id to identify 148 | example: 'zigate.0123456789abcdef' 149 | addr: 150 | description: ZiGate address of the device. 151 | example: 'af7d' 152 | ieee: 153 | description: IEEE address of the device. 154 | example: '0123456789abcdef' 155 | endpoint: 156 | description: Device endpoint. 157 | example: '1' 158 | group_addr: 159 | description: Optional Group addr, if not specified, generate a random addr. 160 | example: '0ab9' 161 | 162 | remove_group: 163 | description: > 164 | Remove group from device 165 | You should provide the entity_id OR the addr OR the ieee. 166 | if group_addr not specified, remove all groups 167 | fields: 168 | entity_id: 169 | description: The device entity_id to identify 170 | example: 'zigate.0123456789abcdef' 171 | addr: 172 | description: ZiGate address of the device. 173 | example: 'af7d' 174 | ieee: 175 | description: IEEE address of the device. 176 | example: '0123456789abcdef' 177 | endpoint: 178 | description: Device endpoint. 179 | example: '1' 180 | group_addr: 181 | description: Optional Group addr, if not specified remove all groups. 182 | example: '0ab9' 183 | 184 | get_group_membership: 185 | description: > 186 | Get device groups 187 | You should provide the entity_id OR the addr OR the ieee. 188 | fields: 189 | entity_id: 190 | description: The device entity_id to identify 191 | example: 'zigate.0123456789abcdef' 192 | addr: 193 | description: ZiGate address of the device. 194 | example: 'af7d' 195 | ieee: 196 | description: IEEE address of the device. 197 | example: '0123456789abcdef' 198 | endpoint: 199 | description: Device endpoint. 200 | example: '1' 201 | 202 | action_onoff: 203 | description: > 204 | Execute action ON/OFF 205 | Note that timed onoff and effect are mutually exclusive 206 | You should provide the entity_id OR the addr OR the ieee. 207 | fields: 208 | entity_id: 209 | description: The device entity_id to identify 210 | example: 'zigate.0123456789abcdef' 211 | addr: 212 | description: ZiGate address of the device or the group 213 | example: 'af7d' 214 | ieee: 215 | description: IEEE address of the device. 216 | example: '0123456789abcdef' 217 | onoff: 218 | description: 0=Off 1=On 2=Toggle 219 | example: '1' 220 | endpoint: 221 | description: Device endpoint (Optional for group addr). 222 | example: '1' 223 | on_time: 224 | description: timed on in sec 225 | example: '0' 226 | off_time: 227 | description: timed off in sec 228 | example: '0' 229 | effect: 230 | description: effect id 231 | example: '0' 232 | gradient: 233 | description: effect gradient 234 | example: '0' 235 | 236 | ota_load_image: 237 | description: > 238 | Load ota image file 239 | fields: 240 | imagepath: 241 | description: Path to ota update file. 242 | example: '/config/ota/10005777-3.1-TRADFRI-control-outlet-2.0.019.ota.ota.signed' 243 | 244 | ota_image_notify: 245 | description: > 246 | Notify OTA Update 247 | You should provide the entity_id OR the addr OR the ieee. 248 | fields: 249 | entity_id: 250 | description: The device entity_id to identify 251 | example: 'zigate.0123456789abcdef' 252 | addr: 253 | description: ZiGate address of the device. 254 | example: 'af7d' 255 | ieee: 256 | description: IEEE address of the device. 257 | example: '0123456789abcdef' 258 | destination_enpoint: 259 | description: Optional destination enpoint 260 | example: '1' 261 | payload_type: 262 | description: Optional payload type 263 | example: '0' 264 | 265 | view_scene: 266 | description: > 267 | View scene 268 | You should provide the entity_id OR the addr OR the ieee. 269 | fields: 270 | entity_id: 271 | description: The device entity_id to identify 272 | example: 'zigate.0123456789abcdef' 273 | enpoint: 274 | description: Enpoint 275 | example: '1' 276 | group_addr: 277 | description: Group addr 278 | example: '0ab9' 279 | scene: 280 | description: Scene id (int) 281 | example: '1' 282 | 283 | add_scene: 284 | description: > 285 | Add scene 286 | You should provide the entity_id OR the addr OR the ieee. 287 | fields: 288 | entity_id: 289 | description: The device entity_id to identify 290 | example: 'zigate.0123456789abcdef' 291 | enpoint: 292 | description: Enpoint 293 | example: '1' 294 | group_addr: 295 | description: Group addr 296 | example: '0ab9' 297 | scene: 298 | description: Scene id (int) 299 | example: '1' 300 | name: 301 | description: Scene name 302 | example: 'Blue' 303 | transition: 304 | description: Transition (optional, default = 0) 305 | example: '0' 306 | 307 | remove_scene: 308 | description: > 309 | Remove scene 310 | You should provide the entity_id OR the addr OR the ieee. 311 | fields: 312 | entity_id: 313 | description: The device entity_id to identify 314 | example: 'zigate.0123456789abcdef' 315 | enpoint: 316 | description: Enpoint 317 | example: '1' 318 | group_addr: 319 | description: Group addr 320 | example: '0ab9' 321 | scene: 322 | description: Scene id (Optional) If not specified remove all scenes 323 | example: '1' 324 | 325 | store_scene: 326 | description: > 327 | Store scene 328 | You should provide the entity_id OR the addr OR the ieee. 329 | fields: 330 | entity_id: 331 | description: The device entity_id to identify 332 | example: 'zigate.0123456789abcdef' 333 | enpoint: 334 | description: Enpoint 335 | example: '1' 336 | group_addr: 337 | description: Group addr 338 | example: '0ab9' 339 | scene: 340 | description: Scene id (int) 341 | example: '1' 342 | 343 | recall_scene: 344 | description: > 345 | Recall scene 346 | You should provide the entity_id OR the addr OR the ieee. 347 | fields: 348 | entity_id: 349 | description: The device entity_id to identify 350 | example: 'zigate.0123456789abcdef' 351 | enpoint: 352 | description: Enpoint 353 | example: '1' 354 | group_addr: 355 | description: Group addr 356 | example: '0ab9' 357 | scene: 358 | description: Scene id (int) 359 | example: '1' 360 | 361 | scene_membership_request: 362 | description: > 363 | Scene Membership request 364 | You should provide the entity_id OR the addr OR the ieee. 365 | fields: 366 | entity_id: 367 | description: The device entity_id to identify 368 | example: 'zigate.0123456789abcdef' 369 | enpoint: 370 | description: Enpoint 371 | example: '1' 372 | group_addr: 373 | description: Group addr 374 | example: '0ab9' 375 | 376 | copy_scene: 377 | description: > 378 | Copy scene 379 | You should provide the entity_id OR the addr OR the ieee. 380 | fields: 381 | entity_id: 382 | description: The device entity_id to identify 383 | example: 'zigate.0123456789abcdef' 384 | enpoint: 385 | description: Enpoint 386 | example: '1' 387 | from_group_addr: 388 | description: Source Group addr 389 | example: '0ab9' 390 | from_scene: 391 | description: Source Scene id 392 | example: '1' 393 | to_group_addr: 394 | description: Destination Group addr 395 | example: '0ab9' 396 | to_scene: 397 | description: Destination Scene id 398 | example: '1' 399 | 400 | build_network_table: 401 | description: > 402 | Build a network table. 403 | fields: 404 | force: 405 | description: Force rebuild the table instead of using cache 406 | example: true 407 | 408 | upgrade_firmware: 409 | description: > 410 | Upgrade ZiGate firmware 411 | If path is not provided, the latest firmware will be downloaded 412 | If you're using PiZiGate, the process is fully automatic 413 | If you're using USB ZiGate, you have to call zigate.stop_zigate first and put ZiGate in download mode 414 | before calling this service 415 | fields: 416 | path: 417 | description: Optional path to firmware file 418 | example: '/home/pi/ZiGate_Coordinator.bin' 419 | 420 | ias_warning: 421 | description: > 422 | Execute IAS warning 423 | You should provide the entity_id OR the addr OR the ieee. 424 | fields: 425 | entity_id: 426 | description: The device entity_id to identify 427 | example: 'zigate.0123456789abcdef' 428 | addr: 429 | description: ZiGate address of the device or the group 430 | example: 'af7d' 431 | ieee: 432 | description: IEEE address of the device. 433 | example: '0123456789abcdef' 434 | endpoint: 435 | description: Device endpoint. 436 | example: '1' 437 | mode: 438 | description: Warning mode stop, burglar, fire, emergency, policepanic, firepanic, emergencypanic 439 | example: 'fire' 440 | strobe: 441 | description: enable strobe 442 | example: true 443 | level: 444 | description: level low, medium, high, veryhigh 445 | example: 'low' 446 | duration: 447 | description: Duration in sec. 448 | example: 5 449 | strobe_cycle: 450 | description: Strobe cycle, 10% step. 451 | example: 10 452 | strobe_level: 453 | description: Strobe level, low, medium, high, veryhigh 454 | example: 'low' 455 | 456 | ias_squawk: 457 | description: > 458 | Execute IAS squawk 459 | You should provide the entity_id OR the addr OR the ieee. 460 | fields: 461 | entity_id: 462 | description: The device entity_id to identify 463 | example: 'zigate.0123456789abcdef' 464 | addr: 465 | description: ZiGate address of the device or the group 466 | example: 'af7d' 467 | ieee: 468 | description: IEEE address of the device. 469 | example: '0123456789abcdef' 470 | endpoint: 471 | description: Device endpoint. 472 | example: '1' 473 | mode: 474 | description: squawk mode armed or disarmed 475 | example: 'armed' 476 | strobe: 477 | description: enable strobe 478 | example: true 479 | level: 480 | description: strobe level, low, medium, high, veryhigh 481 | example: 'low' 482 | -------------------------------------------------------------------------------- /custom_components/zigate/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ZiGate component. 3 | 4 | For more details about this platform, please refer to the documentation 5 | https://home-assistant.io/components/zigate/ 6 | """ 7 | import logging 8 | import voluptuous as vol 9 | import os 10 | import datetime 11 | import zigate 12 | 13 | from homeassistant.exceptions import PlatformNotReady 14 | # from homeassistant import config_entries 15 | from homeassistant.helpers.entity import Entity 16 | from homeassistant.helpers.entity_component import EntityComponent 17 | from homeassistant.components.group import \ 18 | ENTITY_ID_FORMAT as GROUP_ENTITY_ID_FORMAT 19 | from homeassistant.helpers.discovery import load_platform 20 | from homeassistant.helpers.event import track_time_change 21 | from homeassistant.const import (ATTR_BATTERY_LEVEL, CONF_PORT, 22 | CONF_HOST, CONF_SCAN_INTERVAL, 23 | ATTR_ENTITY_ID, 24 | EVENT_HOMEASSISTANT_START, 25 | EVENT_HOMEASSISTANT_STOP) 26 | import homeassistant.helpers.config_validation as cv 27 | from .const import DOMAIN, SCAN_INTERVAL 28 | from .adminpanel import adminpanel_setup 29 | 30 | 31 | _LOGGER = logging.getLogger(__name__) 32 | 33 | DATA_ZIGATE_DEVICES = 'zigate_devices' 34 | DATA_ZIGATE_ATTRS = 'zigate_attributes' 35 | ADDR = 'addr' 36 | IEEE = 'ieee' 37 | 38 | ENTITY_ID_ALL_ZIGATE = GROUP_ENTITY_ID_FORMAT.format('all_zigate') 39 | 40 | SUPPORTED_PLATFORMS = ('sensor', 41 | 'binary_sensor', 42 | 'switch', 43 | 'light', 44 | 'cover', 45 | 'climate', 46 | 'lock') 47 | 48 | CONFIG_SCHEMA = vol.Schema({ 49 | DOMAIN: vol.Schema({ 50 | vol.Optional(CONF_PORT): cv.string, 51 | vol.Optional(CONF_HOST): cv.string, 52 | vol.Optional('channel'): cv.positive_int, 53 | vol.Optional('gpio'): cv.boolean, 54 | vol.Optional('enable_led'): cv.boolean, 55 | vol.Optional('polling'): cv.boolean, 56 | vol.Optional(CONF_SCAN_INTERVAL): cv.positive_int, 57 | vol.Optional('admin_panel'): cv.boolean, 58 | }) 59 | }, extra=vol.ALLOW_EXTRA) 60 | 61 | 62 | REFRESH_DEVICE_SCHEMA = vol.Schema({ 63 | vol.Optional(ADDR): cv.string, 64 | vol.Optional(IEEE): cv.string, 65 | vol.Optional(ATTR_ENTITY_ID): cv.entity_id, 66 | vol.Optional('full'): cv.boolean, 67 | }) 68 | 69 | DISCOVER_DEVICE_SCHEMA = vol.Schema({ 70 | vol.Optional(ADDR): cv.string, 71 | vol.Optional(IEEE): cv.string, 72 | vol.Optional(ATTR_ENTITY_ID): cv.entity_id, 73 | }) 74 | 75 | RAW_COMMAND_SCHEMA = vol.Schema({ 76 | vol.Required('cmd'): cv.string, 77 | vol.Optional('data'): cv.string, 78 | }) 79 | 80 | IDENTIFY_SCHEMA = vol.Schema({ 81 | vol.Optional(ADDR): cv.string, 82 | vol.Optional(IEEE): cv.string, 83 | vol.Optional(ATTR_ENTITY_ID): cv.entity_id, 84 | }) 85 | 86 | REMOVE_SCHEMA = vol.Schema({ 87 | vol.Optional(ADDR): cv.string, 88 | vol.Optional(IEEE): cv.string, 89 | vol.Optional(ATTR_ENTITY_ID): cv.entity_id, 90 | }) 91 | 92 | READ_ATTRIBUTE_SCHEMA = vol.Schema({ 93 | vol.Optional(ADDR): cv.string, 94 | vol.Optional(IEEE): cv.string, 95 | vol.Optional(ATTR_ENTITY_ID): cv.entity_id, 96 | vol.Required('endpoint'): cv.string, 97 | vol.Required('cluster'): cv.string, 98 | vol.Required('attribute_id'): cv.string, 99 | vol.Optional('manufacturer_code'): cv.string, 100 | }) 101 | 102 | WRITE_ATTRIBUTE_SCHEMA = vol.Schema({ 103 | vol.Optional(ADDR): cv.string, 104 | vol.Optional(IEEE): cv.string, 105 | vol.Optional(ATTR_ENTITY_ID): cv.entity_id, 106 | vol.Required('endpoint'): cv.string, 107 | vol.Required('cluster'): cv.string, 108 | vol.Required('attribute_id'): cv.string, 109 | vol.Required('attribute_type'): cv.string, 110 | vol.Required('value'): cv.string, 111 | vol.Optional('manufacturer_code'): cv.string, 112 | }) 113 | 114 | ADD_GROUP_SCHEMA = vol.Schema({ 115 | vol.Optional(ADDR): cv.string, 116 | vol.Optional(IEEE): cv.string, 117 | vol.Optional(ATTR_ENTITY_ID): cv.entity_id, 118 | vol.Required('endpoint'): cv.string, 119 | vol.Optional('group_addr'): cv.string, 120 | }) 121 | 122 | REMOVE_GROUP_SCHEMA = vol.Schema({ 123 | vol.Optional(ADDR): cv.string, 124 | vol.Optional(IEEE): cv.string, 125 | vol.Optional(ATTR_ENTITY_ID): cv.entity_id, 126 | vol.Required('endpoint'): cv.string, 127 | vol.Optional('group_addr'): cv.string, 128 | }) 129 | 130 | GET_GROUP_MEMBERSHIP_SCHEMA = vol.Schema({ 131 | vol.Optional(ADDR): cv.string, 132 | vol.Optional(IEEE): cv.string, 133 | vol.Optional(ATTR_ENTITY_ID): cv.entity_id, 134 | vol.Required('endpoint'): cv.string, 135 | }) 136 | 137 | ACTION_ONOFF_SCHEMA = vol.Schema({ 138 | vol.Optional(ADDR): cv.string, 139 | vol.Optional(IEEE): cv.string, 140 | vol.Optional(ATTR_ENTITY_ID): cv.entity_id, 141 | vol.Required('onoff'): cv.string, 142 | vol.Optional('endpoint'): cv.string, 143 | vol.Optional('on_time'): cv.string, 144 | vol.Optional('off_time'): cv.string, 145 | vol.Optional('effect'): cv.string, 146 | vol.Optional('gradient'): cv.string, 147 | }) 148 | 149 | OTA_LOAD_IMAGE_SCHEMA = vol.Schema({ 150 | vol.Required('imagepath'): cv.string, 151 | }) 152 | 153 | OTA_IMAGE_NOTIFY_SCHEMA = vol.Schema({ 154 | vol.Optional(ADDR): cv.string, 155 | vol.Optional(IEEE): cv.string, 156 | vol.Optional(ATTR_ENTITY_ID): cv.entity_id, 157 | vol.Optional('destination_enpoint'): cv.string, 158 | vol.Optional('payload_type'): cv.string, 159 | }) 160 | 161 | VIEW_SCENE_SCHEMA = vol.Schema({ 162 | vol.Optional(ADDR): cv.string, 163 | vol.Optional(IEEE): cv.string, 164 | vol.Optional(ATTR_ENTITY_ID): cv.entity_id, 165 | vol.Required('endpoint'): cv.string, 166 | vol.Required('group_addr'): cv.string, 167 | vol.Required('scene'): cv.string, 168 | }) 169 | 170 | ADD_SCENE_SCHEMA = vol.Schema({ 171 | vol.Optional(ADDR): cv.string, 172 | vol.Optional(IEEE): cv.string, 173 | vol.Optional(ATTR_ENTITY_ID): cv.entity_id, 174 | vol.Required('endpoint'): cv.string, 175 | vol.Required('group_addr'): cv.string, 176 | vol.Required('scene'): cv.string, 177 | vol.Required('name'): cv.string, 178 | vol.Optional('transition'): cv.string, 179 | }) 180 | 181 | REMOVE_SCENE_SCHEMA = vol.Schema({ 182 | vol.Optional(ADDR): cv.string, 183 | vol.Optional(IEEE): cv.string, 184 | vol.Optional(ATTR_ENTITY_ID): cv.entity_id, 185 | vol.Required('endpoint'): cv.string, 186 | vol.Required('group_addr'): cv.string, 187 | vol.Optional('scene'): cv.string, 188 | }) 189 | 190 | STORE_SCENE_SCHEMA = vol.Schema({ 191 | vol.Optional(ADDR): cv.string, 192 | vol.Optional(IEEE): cv.string, 193 | vol.Optional(ATTR_ENTITY_ID): cv.entity_id, 194 | vol.Required('endpoint'): cv.string, 195 | vol.Required('group_addr'): cv.string, 196 | vol.Required('scene'): cv.string, 197 | }) 198 | 199 | RECALL_SCENE_SCHEMA = vol.Schema({ 200 | vol.Optional(ADDR): cv.string, 201 | vol.Optional(IEEE): cv.string, 202 | vol.Optional(ATTR_ENTITY_ID): cv.entity_id, 203 | vol.Required('endpoint'): cv.string, 204 | vol.Required('group_addr'): cv.string, 205 | vol.Required('scene'): cv.string, 206 | }) 207 | 208 | SCENE_MEMBERSHIP_REQUEST_SCHEMA = vol.Schema({ 209 | vol.Optional(ADDR): cv.string, 210 | vol.Optional(IEEE): cv.string, 211 | vol.Optional(ATTR_ENTITY_ID): cv.entity_id, 212 | vol.Required('endpoint'): cv.string, 213 | vol.Required('group_addr'): cv.string, 214 | }) 215 | 216 | COPY_SCENE_SCHEMA = vol.Schema({ 217 | vol.Optional(ADDR): cv.string, 218 | vol.Optional(IEEE): cv.string, 219 | vol.Optional(ATTR_ENTITY_ID): cv.entity_id, 220 | vol.Required('endpoint'): cv.string, 221 | vol.Required('from_group_addr'): cv.string, 222 | vol.Required('from_scene'): cv.string, 223 | vol.Required('to_group_addr'): cv.string, 224 | vol.Required('to_scene'): cv.string, 225 | }) 226 | 227 | BUILD_NETWORK_TABLE_SCHEMA = vol.Schema({ 228 | vol.Optional('force'): cv.boolean, 229 | }) 230 | 231 | ACTION_IAS_WARNING_SCHEMA = vol.Schema({ 232 | vol.Optional(ADDR): cv.string, 233 | vol.Optional(IEEE): cv.string, 234 | vol.Optional(ATTR_ENTITY_ID): cv.entity_id, 235 | vol.Required('endpoint'): cv.string, 236 | vol.Optional('mode'): cv.string, 237 | vol.Optional('strobe'): cv.boolean, 238 | vol.Optional('level'): cv.string, 239 | vol.Optional('duration'): cv.positive_int, 240 | vol.Optional('strobe_cycle'): cv.positive_int, 241 | vol.Optional('strobe_level'): cv.string, 242 | }) 243 | 244 | ACTION_IAS_SQUAWK_SCHEMA = vol.Schema({ 245 | vol.Optional(ADDR): cv.string, 246 | vol.Optional(IEEE): cv.string, 247 | vol.Optional(ATTR_ENTITY_ID): cv.entity_id, 248 | vol.Required('endpoint'): cv.string, 249 | vol.Optional('mode'): cv.string, 250 | vol.Optional('strobe'): cv.boolean, 251 | vol.Optional('level'): cv.string, 252 | }) 253 | 254 | 255 | def setup(hass, config): 256 | """Setup zigate platform.""" 257 | port = config[DOMAIN].get(CONF_PORT) 258 | host = config[DOMAIN].get(CONF_HOST) 259 | gpio = config[DOMAIN].get('gpio', False) 260 | enable_led = config[DOMAIN].get('enable_led', True) 261 | polling = config[DOMAIN].get('polling', True) 262 | channel = config[DOMAIN].get('channel') 263 | scan_interval = datetime.timedelta(seconds=config[DOMAIN].get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)) 264 | admin_panel = config[DOMAIN].get('admin_panel', True) 265 | 266 | persistent_file = os.path.join(hass.config.config_dir, 267 | 'zigate.json') 268 | 269 | _LOGGER.debug('Port : %s', port) 270 | _LOGGER.debug('Host : %s', host) 271 | _LOGGER.debug('GPIO : %s', gpio) 272 | _LOGGER.debug('Led : %s', enable_led) 273 | _LOGGER.debug('Channel : %s', channel) 274 | _LOGGER.debug('Scan interval : %s', scan_interval) 275 | 276 | myzigate = zigate.connect(port=port, host=host, 277 | path=persistent_file, 278 | auto_start=False, 279 | gpio=gpio 280 | ) 281 | _LOGGER.debug('ZiGate object created %s', myzigate) 282 | 283 | hass.data[DOMAIN] = myzigate 284 | hass.data[DATA_ZIGATE_DEVICES] = {} 285 | hass.data[DATA_ZIGATE_ATTRS] = {} 286 | 287 | component = EntityComponent(_LOGGER, DOMAIN, hass, scan_interval) 288 | # component.setup(config) 289 | entity = ZiGateComponentEntity(myzigate) 290 | hass.data[DATA_ZIGATE_DEVICES]['zigate'] = entity 291 | component.add_entities([entity]) 292 | 293 | def device_added(**kwargs): 294 | device = kwargs['device'] 295 | _LOGGER.debug('Add device {}'.format(device)) 296 | ieee = device.ieee 297 | if ieee not in hass.data[DATA_ZIGATE_DEVICES]: 298 | hass.data[DATA_ZIGATE_DEVICES][ieee] = None # reserve 299 | entity = ZiGateDeviceEntity(hass, device, polling) 300 | hass.data[DATA_ZIGATE_DEVICES][ieee] = entity 301 | component.add_entities([entity]) 302 | if 'signal' in kwargs: 303 | hass.components.persistent_notification.create( 304 | ('A new ZiGate device "{}"' 305 | ' has been added !' 306 | ).format(device), 307 | title='ZiGate') 308 | 309 | def device_removed(**kwargs): 310 | # component.async_remove_entity 311 | device = kwargs['device'] 312 | ieee = device.ieee 313 | hass.components.persistent_notification.create( 314 | 'The ZiGate device {}({}) is gone.'.format(device.ieee, 315 | device.addr), 316 | title='ZiGate') 317 | entity = hass.data[DATA_ZIGATE_DEVICES][ieee] 318 | component.async_remove_entity(entity.entity_id) 319 | del hass.data[DATA_ZIGATE_DEVICES][ieee] 320 | 321 | def device_need_discovery(**kwargs): 322 | device = kwargs['device'] 323 | hass.components.persistent_notification.create( 324 | ('The ZiGate device {}({}) needs to be discovered' 325 | ' (missing important' 326 | ' information)').format(device.ieee, device.addr), 327 | title='ZiGate') 328 | 329 | zigate.dispatcher.connect(device_added, 330 | zigate.ZIGATE_DEVICE_ADDED, weak=False) 331 | zigate.dispatcher.connect(device_removed, 332 | zigate.ZIGATE_DEVICE_REMOVED, weak=False) 333 | zigate.dispatcher.connect(device_need_discovery, 334 | zigate.ZIGATE_DEVICE_NEED_DISCOVERY, weak=False) 335 | 336 | def attribute_updated(**kwargs): 337 | device = kwargs['device'] 338 | ieee = device.ieee 339 | attribute = kwargs['attribute'] 340 | _LOGGER.debug('Update attribute for device {} {}'.format(device, 341 | attribute)) 342 | entity = hass.data[DATA_ZIGATE_DEVICES].get(ieee) 343 | event_data = attribute.copy() 344 | if type(event_data.get('type')) == type: 345 | event_data['type'] = event_data['type'].__name__ 346 | event_data['ieee'] = device.ieee 347 | event_data['addr'] = device.addr 348 | event_data['device_type'] = device.get_property_value('type') 349 | if entity: 350 | event_data['entity_id'] = entity.entity_id 351 | hass.bus.fire('zigate.attribute_updated', event_data) 352 | 353 | zigate.dispatcher.connect(attribute_updated, 354 | zigate.ZIGATE_ATTRIBUTE_UPDATED, weak=False) 355 | 356 | def device_updated(**kwargs): 357 | device = kwargs['device'] 358 | _LOGGER.debug('Update device {}'.format(device)) 359 | ieee = device.ieee 360 | entity = hass.data[DATA_ZIGATE_DEVICES].get(ieee) 361 | if not entity: 362 | _LOGGER.debug('Device not found {}, adding it'.format(device)) 363 | device_added(device=device) 364 | event_data = {} 365 | event_data['ieee'] = device.ieee 366 | event_data['addr'] = device.addr 367 | event_data['device_type'] = device.get_property_value('type') 368 | if entity: 369 | event_data['entity_id'] = entity.entity_id 370 | hass.bus.fire('zigate.device_updated', event_data) 371 | 372 | zigate.dispatcher.connect(device_updated, 373 | zigate.ZIGATE_DEVICE_UPDATED, weak=False) 374 | zigate.dispatcher.connect(device_updated, 375 | zigate.ZIGATE_ATTRIBUTE_ADDED, weak=False) 376 | zigate.dispatcher.connect(device_updated, 377 | zigate.ZIGATE_DEVICE_ADDRESS_CHANGED, weak=False) 378 | 379 | def zigate_reset(service): 380 | myzigate.reset() 381 | 382 | def permit_join(service): 383 | myzigate.permit_join() 384 | 385 | def zigate_cleanup(service): 386 | ''' 387 | Remove missing device 388 | ''' 389 | myzigate.cleanup_devices() 390 | 391 | def start_zigate(service_event=None): 392 | myzigate.autoStart(channel) 393 | myzigate.start_auto_save() 394 | myzigate.set_led(enable_led) 395 | version = myzigate.get_version_text() 396 | if version < '3.1a': 397 | hass.components.persistent_notification.create( 398 | ('Your zigate firmware is outdated, ' 399 | 'Please upgrade to 3.1a or later !'), 400 | title='ZiGate') 401 | # first load 402 | for device in myzigate.devices: 403 | device_added(device=device) 404 | 405 | for platform in SUPPORTED_PLATFORMS: 406 | load_platform(hass, platform, DOMAIN, {}, config) 407 | 408 | hass.bus.fire('zigate.started') 409 | 410 | def stop_zigate(service=None): 411 | myzigate.save_state() 412 | myzigate.close() 413 | 414 | hass.bus.fire('zigate.stopped') 415 | 416 | def refresh_devices_list(service): 417 | myzigate.get_devices_list() 418 | 419 | def generate_templates(service): 420 | myzigate.generate_templates(hass.config.config_dir) 421 | 422 | def _get_addr_from_service_request(service): 423 | entity_id = service.data.get(ATTR_ENTITY_ID) 424 | ieee = service.data.get(IEEE) 425 | addr = service.data.get(ADDR) 426 | if entity_id: 427 | entity = component.get_entity(entity_id) 428 | if entity: 429 | addr = entity._device.addr 430 | elif ieee: 431 | device = myzigate.get_device_from_ieee(ieee) 432 | if device: 433 | addr = device.addr 434 | return addr 435 | 436 | def _to_int(value): 437 | ''' 438 | convert str to int 439 | ''' 440 | if 'x' in value: 441 | return int(value, 16) 442 | return int(value) 443 | 444 | def refresh_device(service): 445 | full = service.data.get('full', False) 446 | addr = _get_addr_from_service_request(service) 447 | if addr: 448 | myzigate.refresh_device(addr, full=full) 449 | else: 450 | for device in myzigate.devices: 451 | device.refresh_device(full=full) 452 | 453 | def discover_device(service): 454 | addr = _get_addr_from_service_request(service) 455 | if addr: 456 | myzigate.discover_device(addr, True) 457 | 458 | def network_scan(service): 459 | myzigate.start_network_scan() 460 | 461 | def raw_command(service): 462 | cmd = _to_int(service.data.get('cmd')) 463 | data = service.data.get('data', '') 464 | myzigate.send_data(cmd, data) 465 | 466 | def identify_device(service): 467 | addr = _get_addr_from_service_request(service) 468 | myzigate.identify_device(addr) 469 | 470 | def remove_device(service): 471 | addr = _get_addr_from_service_request(service) 472 | myzigate.remove_device(addr) 473 | 474 | def initiate_touchlink(service): 475 | myzigate.initiate_touchlink() 476 | 477 | def touchlink_factory_reset(service): 478 | myzigate.touchlink_factory_reset() 479 | 480 | def read_attribute(service): 481 | addr = _get_addr_from_service_request(service) 482 | endpoint = _to_int(service.data.get('endpoint')) 483 | cluster = _to_int(service.data.get('cluster')) 484 | attribute_id = _to_int(service.data.get('attribute_id')) 485 | manufacturer_code = _to_int(service.data.get('manufacturer_code', '0')) 486 | myzigate.read_attribute_request(addr, endpoint, cluster, attribute_id, 487 | manufacturer_code=manufacturer_code) 488 | 489 | def write_attribute(service): 490 | addr = _get_addr_from_service_request(service) 491 | endpoint = _to_int(service.data.get('endpoint')) 492 | cluster = _to_int(service.data.get('cluster')) 493 | attribute_id = _to_int(service.data.get('attribute_id')) 494 | attribute_type = _to_int(service.data.get('attribute_type')) 495 | value = _to_int(service.data.get('value')) 496 | attributes = [(attribute_id, attribute_type, value)] 497 | manufacturer_code = _to_int(service.data.get('manufacturer_code', '0')) 498 | myzigate.write_attribute_request(addr, endpoint, cluster, attributes, 499 | manufacturer_code=manufacturer_code) 500 | 501 | def add_group(service): 502 | addr = _get_addr_from_service_request(service) 503 | endpoint = _to_int(service.data.get('endpoint')) 504 | groupaddr = service.data.get('group_addr') 505 | myzigate.add_group(addr, endpoint, groupaddr) 506 | 507 | def remove_group(service): 508 | addr = _get_addr_from_service_request(service) 509 | endpoint = _to_int(service.data.get('endpoint')) 510 | groupaddr = service.data.get('group_addr') 511 | myzigate.remove_group(addr, endpoint, groupaddr) 512 | 513 | def get_group_membership(service): 514 | addr = _get_addr_from_service_request(service) 515 | endpoint = _to_int(service.data.get('endpoint')) 516 | myzigate.get_group_membership(addr, endpoint) 517 | 518 | def action_onoff(service): 519 | addr = _get_addr_from_service_request(service) 520 | onoff = _to_int(service.data.get('onoff')) 521 | endpoint = _to_int(service.data.get('endpoint', '0')) 522 | ontime = _to_int(service.data.get('on_time', '0')) 523 | offtime = _to_int(service.data.get('off_time', '0')) 524 | effect = _to_int(service.data.get('effect', '0')) 525 | gradient = _to_int(service.data.get('gradient', '0')) 526 | myzigate.action_onoff(addr, endpoint, onoff, ontime, offtime, effect, gradient) 527 | 528 | def build_network_table(service): 529 | table = myzigate.build_neighbours_table(service.data.get('force', False)) 530 | _LOGGER.debug('Neighbours table {}'.format(table)) 531 | 532 | def ota_load_image(service): 533 | ota_image_path = service.data.get('imagepath') 534 | myzigate.ota_load_image(ota_image_path) 535 | 536 | def ota_image_notify(service): 537 | addr = _get_addr_from_service_request(service) 538 | destination_endpoint = _to_int(service.data.get('destination_endpoint', '1')) 539 | payload_type = _to_int(service.data.get('payload_type', '0')) 540 | myzigate.ota_image_notify(addr, destination_endpoint, payload_type) 541 | 542 | def get_ota_status(service): 543 | myzigate.get_ota_status() 544 | 545 | def view_scene(service): 546 | addr = _get_addr_from_service_request(service) 547 | endpoint = _to_int(service.data.get('endpoint', '1')) 548 | groupaddr = service.data.get('group_addr') 549 | scene = _to_int(service.data.get('scene')) 550 | myzigate.view_scene(addr, endpoint, groupaddr, scene) 551 | 552 | def add_scene(service): 553 | addr = _get_addr_from_service_request(service) 554 | endpoint = _to_int(service.data.get('endpoint', '1')) 555 | groupaddr = service.data.get('group_addr') 556 | scene = _to_int(service.data.get('scene')) 557 | name = service.data.get('scene_name') 558 | transition = _to_int(service.data.get('transition', '0')) 559 | myzigate.add_scene(addr, endpoint, groupaddr, scene, name, transition) 560 | 561 | def remove_scene(service): 562 | addr = _get_addr_from_service_request(service) 563 | endpoint = _to_int(service.data.get('endpoint', '1')) 564 | groupaddr = service.data.get('group_addr') 565 | scene = _to_int(service.data.get('scene', -1)) 566 | if scene == -1: 567 | scene = None 568 | myzigate.remove_scene(addr, endpoint, groupaddr, scene) 569 | 570 | def store_scene(service): 571 | addr = _get_addr_from_service_request(service) 572 | endpoint = _to_int(service.data.get('endpoint', '1')) 573 | groupaddr = service.data.get('group_addr') 574 | scene = _to_int(service.data.get('scene')) 575 | myzigate.store_scene(addr, endpoint, groupaddr, scene) 576 | 577 | def recall_scene(service): 578 | addr = _get_addr_from_service_request(service) 579 | endpoint = _to_int(service.data.get('endpoint', '1')) 580 | groupaddr = service.data.get('group_addr') 581 | scene = _to_int(service.data.get('scene')) 582 | myzigate.recall_scene(addr, endpoint, groupaddr, scene) 583 | 584 | def scene_membership_request(service): 585 | addr = _get_addr_from_service_request(service) 586 | endpoint = _to_int(service.data.get('endpoint', '1')) 587 | groupaddr = service.data.get('group_addr') 588 | myzigate.scene_membership_request(addr, endpoint, groupaddr) 589 | 590 | def copy_scene(service): 591 | addr = _get_addr_from_service_request(service) 592 | endpoint = _to_int(service.data.get('endpoint', '1')) 593 | fromgroupaddr = service.data.get('from_group_addr') 594 | fromscene = _to_int(service.data.get('from_scene')) 595 | togroupaddr = service.data.get('to_group_addr') 596 | toscene = _to_int(service.data.get('to_scene')) 597 | myzigate.copy_scene(addr, endpoint, fromgroupaddr, fromscene, togroupaddr, toscene) 598 | 599 | def ias_warning(service): 600 | addr = _get_addr_from_service_request(service) 601 | endpoint = _to_int(service.data.get('endpoint', '1')) 602 | mode = service.data.get('mode', 'burglar') 603 | strobe = service.data.get('strobe', True) 604 | level = service.data.get('level', 'low') 605 | duration = service.data.get('duration', 60) 606 | strobe_cycle = service.data.get('strobe_cycle', 10) 607 | strobe_level = service.data.get('strobe_level', 'low') 608 | myzigate.action_ias_warning(addr, endpoint, mode, strobe, level, duration, strobe_cycle, strobe_level) 609 | 610 | def ias_squawk(service): 611 | addr = _get_addr_from_service_request(service) 612 | endpoint = _to_int(service.data.get('endpoint', '1')) 613 | mode = service.data.get('mode', 'armed') 614 | strobe = service.data.get('strobe', True) 615 | level = service.data.get('level', 'low') 616 | myzigate.action_ias_squawk(addr, endpoint, mode, strobe, level) 617 | 618 | def upgrade_firmware(service): 619 | from zigate.flasher import flash 620 | from zigate.firmware import download_latest 621 | port = myzigate._port 622 | pizigate = False 623 | if isinstance(myzigate, zigate.ZiGateGPIO): 624 | pizigate = True 625 | if myzigate._started and not pizigate: 626 | msg = 'You should stop zigate first using service zigate.stop_zigate and put zigate in download mode.' 627 | hass.components.persistent_notification.create(msg, title='ZiGate') 628 | return 629 | if pizigate: 630 | stop_zigate() 631 | myzigate.set_bootloader_mode() 632 | backup_filename = 'zigate_backup_{:%Y%m%d%H%M%S}.bin'.format(datetime.datetime.now()) 633 | backup_filename = os.path.join(hass.config.config_dir, backup_filename) 634 | flash(port, save=backup_filename) 635 | msg = 'ZiGate backup created {}'.format(backup_filename) 636 | hass.components.persistent_notification.create(msg, title='ZiGate') 637 | firmware_path = service.data.get('path') 638 | if not firmware_path: 639 | firmware_path = download_latest() 640 | flash(port, write=firmware_path) 641 | msg = 'ZiGate flashed with {}'.format(firmware_path) 642 | hass.components.persistent_notification.create(msg, title='ZiGate') 643 | myzigate._version = None 644 | if pizigate: 645 | myzigate.set_running_mode() 646 | start_zigate() 647 | else: 648 | msg = 'Now you have to unplug/replug the ZiGate USB key and then call service zigate.start_zigate' 649 | hass.components.persistent_notification.create(msg, title='ZiGate') 650 | 651 | hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_zigate) 652 | hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zigate) 653 | 654 | hass.services.register(DOMAIN, 'refresh_devices_list', 655 | refresh_devices_list) 656 | hass.services.register(DOMAIN, 'generate_templates', 657 | generate_templates) 658 | hass.services.register(DOMAIN, 'reset', zigate_reset) 659 | hass.services.register(DOMAIN, 'permit_join', permit_join) 660 | hass.services.register(DOMAIN, 'start_zigate', start_zigate) 661 | hass.services.register(DOMAIN, 'stop_zigate', stop_zigate) 662 | hass.services.register(DOMAIN, 'cleanup_devices', zigate_cleanup) 663 | hass.services.register(DOMAIN, 'refresh_device', 664 | refresh_device, 665 | schema=REFRESH_DEVICE_SCHEMA) 666 | hass.services.register(DOMAIN, 'discover_device', 667 | discover_device, 668 | schema=DISCOVER_DEVICE_SCHEMA) 669 | hass.services.register(DOMAIN, 'network_scan', network_scan) 670 | hass.services.register(DOMAIN, 'raw_command', raw_command, 671 | schema=RAW_COMMAND_SCHEMA) 672 | hass.services.register(DOMAIN, 'identify_device', identify_device, 673 | schema=IDENTIFY_SCHEMA) 674 | hass.services.register(DOMAIN, 'remove_device', remove_device, 675 | schema=REMOVE_SCHEMA) 676 | hass.services.register(DOMAIN, 'initiate_touchlink', initiate_touchlink) 677 | hass.services.register(DOMAIN, 'touchlink_factory_reset', 678 | touchlink_factory_reset) 679 | hass.services.register(DOMAIN, 'read_attribute', read_attribute, 680 | schema=READ_ATTRIBUTE_SCHEMA) 681 | hass.services.register(DOMAIN, 'write_attribute', write_attribute, 682 | schema=WRITE_ATTRIBUTE_SCHEMA) 683 | hass.services.register(DOMAIN, 'add_group', add_group, 684 | schema=ADD_GROUP_SCHEMA) 685 | hass.services.register(DOMAIN, 'get_group_membership', get_group_membership, 686 | schema=GET_GROUP_MEMBERSHIP_SCHEMA) 687 | hass.services.register(DOMAIN, 'remove_group', remove_group, 688 | schema=REMOVE_GROUP_SCHEMA) 689 | hass.services.register(DOMAIN, 'action_onoff', action_onoff, 690 | schema=ACTION_ONOFF_SCHEMA) 691 | hass.services.register(DOMAIN, 'build_network_table', build_network_table, 692 | schema=BUILD_NETWORK_TABLE_SCHEMA) 693 | hass.services.register(DOMAIN, 'ias_warning', ias_warning, 694 | schema=ACTION_IAS_WARNING_SCHEMA) 695 | hass.services.register(DOMAIN, 'ias_squawk', ias_squawk, 696 | schema=ACTION_IAS_SQUAWK_SCHEMA) 697 | 698 | hass.services.register(DOMAIN, 'ota_load_image', ota_load_image, 699 | schema=OTA_LOAD_IMAGE_SCHEMA) 700 | hass.services.register(DOMAIN, 'ota_image_notify', ota_image_notify, 701 | schema=OTA_IMAGE_NOTIFY_SCHEMA) 702 | hass.services.register(DOMAIN, 'ota_get_status', get_ota_status) 703 | hass.services.register(DOMAIN, 'view_scene', view_scene, 704 | schema=VIEW_SCENE_SCHEMA) 705 | hass.services.register(DOMAIN, 'add_scene', add_scene, 706 | schema=ADD_SCENE_SCHEMA) 707 | hass.services.register(DOMAIN, 'remove_scene', remove_scene, 708 | schema=REMOVE_SCENE_SCHEMA) 709 | hass.services.register(DOMAIN, 'store_scene', store_scene, 710 | schema=STORE_SCENE_SCHEMA) 711 | hass.services.register(DOMAIN, 'recall_scene', recall_scene, 712 | schema=RECALL_SCENE_SCHEMA) 713 | hass.services.register(DOMAIN, 'scene_membership_request', scene_membership_request, 714 | schema=SCENE_MEMBERSHIP_REQUEST_SCHEMA) 715 | hass.services.register(DOMAIN, 'copy_scene', copy_scene, 716 | schema=COPY_SCENE_SCHEMA) 717 | hass.services.register(DOMAIN, 'upgrade_firmware', upgrade_firmware) 718 | track_time_change(hass, refresh_devices_list, 719 | hour=0, minute=0, second=0) 720 | 721 | if admin_panel: 722 | _LOGGER.debug('Start ZiGate Admin Panel on port 9998') 723 | myzigate.start_adminpanel() 724 | # myzigate.start_adminpanel(mount='/zigateproxy') 725 | # adminpanel_setup(hass, 'zigateproxy') 726 | 727 | # hass.async_create_task( 728 | # hass.config_entries.flow.async_init( 729 | # DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={} 730 | # ) 731 | # ) 732 | 733 | return True 734 | 735 | 736 | # async def async_setup_entry(hass, entry): 737 | # _LOGGER.warning('async_setup_entry not implemented yet for ZiGate') 738 | # return False 739 | 740 | 741 | class ZiGateComponentEntity(Entity): 742 | '''Representation of ZiGate Key''' 743 | def __init__(self, myzigate): 744 | """Initialize the sensor.""" 745 | self._device = myzigate 746 | self.entity_id = '{}.{}'.format(DOMAIN, 'zigate') 747 | 748 | @property 749 | def network_table(self): 750 | return self._device._neighbours_table_cache 751 | 752 | @property 753 | def should_poll(self): 754 | """No polling.""" 755 | return True 756 | 757 | @property 758 | def name(self): 759 | """Return the name of the sensor.""" 760 | return 'ZiGate' 761 | 762 | @property 763 | def state(self): 764 | """Return the state of the sensor.""" 765 | if self._device.connection: 766 | if self._device.connection.is_connected(): 767 | return 'connected' 768 | return 'disconnected' 769 | 770 | @property 771 | def unique_id(self) -> str: 772 | return self._device.ieee 773 | 774 | @property 775 | def device_state_attributes(self): 776 | """Return the device specific state attributes.""" 777 | if not self._device.connection: 778 | return {} 779 | attrs = {'addr': self._device.addr, 780 | 'ieee': self._device.ieee, 781 | 'groups': self._device.groups, 782 | 'network_table': self.network_table, 783 | 'firmware_version': self._device.get_version_text(), 784 | 'lib version': zigate.__version__ 785 | } 786 | return attrs 787 | 788 | @property 789 | def icon(self): 790 | return 'mdi:zigbee' 791 | 792 | 793 | class ZiGateDeviceEntity(Entity): 794 | '''Representation of ZiGate device''' 795 | 796 | def __init__(self, hass, device, polling=True): 797 | """Initialize the sensor.""" 798 | self._polling = polling 799 | self._device = device 800 | ieee = device.ieee or device.addr 801 | self.entity_id = '{}.{}'.format(DOMAIN, ieee) 802 | hass.bus.listen('zigate.attribute_updated', self._handle_event) 803 | hass.bus.listen('zigate.device_updated', self._handle_event) 804 | 805 | def _handle_event(self, call): 806 | if self._device.ieee == call.data['ieee']: 807 | if not self.hass: 808 | raise PlatformNotReady 809 | self.schedule_update_ha_state() 810 | 811 | @property 812 | def should_poll(self): 813 | """No polling.""" 814 | return self._polling and self._device.receiver_on_when_idle() 815 | 816 | def update(self): 817 | self._device.refresh_device() 818 | 819 | @property 820 | def name(self): 821 | """Return the name of the sensor.""" 822 | return str(self._device) 823 | 824 | @property 825 | def state(self): 826 | """Return the state of the sensor.""" 827 | return self._device.info.get('last_seen') 828 | 829 | @property 830 | def unique_id(self) -> str: 831 | return self._device.ieee 832 | 833 | @property 834 | def device_state_attributes(self): 835 | """Return the device specific state attributes.""" 836 | attrs = {'lqi_percent': int(self._device.lqi_percent), 837 | 'type': self._device.get_value('type'), 838 | 'manufacturer': self._device.get_value('manufacturer'), 839 | 'receiver_on_when_idle': self._device.receiver_on_when_idle(), 840 | 'missing': self._device.missing, 841 | 'generic_type': self._device.genericType, 842 | 'discovery': self._device.discovery, 843 | 'groups': self._device.groups, 844 | 'datecode': self._device.get_value('datecode') 845 | } 846 | if not self._device.receiver_on_when_idle(): 847 | attrs.update({'battery_voltage': self._device.get_value('battery_voltage'), 848 | ATTR_BATTERY_LEVEL: int(self._device.battery_percent), 849 | }) 850 | attrs.update(self._device.info) 851 | return attrs 852 | 853 | @property 854 | def icon(self): 855 | if self._device.missing: 856 | return 'mdi:emoticon-dead' 857 | if self.state: 858 | last_24h = datetime.datetime.now() - datetime.timedelta(hours=24) 859 | last_24h = last_24h.strftime('%Y-%m-%d %H:%M:%S') 860 | if not self.state or self.state < last_24h: 861 | return 'mdi:help' 862 | return 'mdi:access-point' 863 | 864 | @property 865 | def available(self): 866 | return not self._device.missing 867 | 868 | @property 869 | def device_info(self): 870 | return { 871 | "identifiers": { 872 | (DOMAIN, self.unique_id) 873 | }, 874 | "name": self.name, 875 | "manufacturer": self._device.get_value('manufacturer'), 876 | "model": self._device.get_value('type'), 877 | "sw_version": self._device.get_value('datecode') 878 | } 879 | --------------------------------------------------------------------------------