diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 30e1638..19ba6f9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -65,13 +65,13 @@ repos: hooks: - id: beautysh - repo: https://github.com/asottile/pyupgrade - rev: v2.38.2 + rev: v3.1.0 hooks: - id: pyupgrade args: - --py37-plus - repo: https://github.com/psf/black - rev: 22.8.0 + rev: 22.10.0 hooks: - id: black args: @@ -110,7 +110,7 @@ repos: hooks: - id: isort - repo: https://github.com/codespell-project/codespell - rev: v2.2.1 + rev: v2.2.2 hooks: - id: codespell args: @@ -128,7 +128,7 @@ repos: - --py-version=3.7 # exclude: ^$ - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.981 + rev: v0.982 hooks: - id: mypy args: diff --git a/README.md b/README.md index e511bc2..5ef9df3 100644 --- a/README.md +++ b/README.md @@ -199,12 +199,13 @@ test the service first before adding them to an automation. Go to Developer Tools > Services in your instance : [![Open your Home Assistant instance and show your service developer tools.](https://my.home-assistant.io/badges/developer_services.svg)](https://my.home-assistant.io/redirect/developer_services/). -Choose `zha_toolkit.execute` as the service.\ -Most parameters can be set -using the UI, there are some cases where you may want to enable Yaml entry -\- you'll have some more flexibility and all parameters fit in your browser -view. On the other hand, the UI interface makes it easier to select the -entity. You can switch back and forth! +Choose the generic service `zha_toolkit.execute` or - more convenient - the +specific `zha_toolkit.` as the service.\ +Most parameters can be +set using the UI, there are some cases where you may want to enable Yaml +entry - you'll have some more flexibility and all parameters fit in your +browser view. On the other hand, the UI interface makes it easier to select +the entity. You can switch back and forth! There are several examples below for different commands. You can copy/paste them to start from. @@ -251,11 +252,15 @@ service: zha_toolkit.SOME_SERVICE # Valid possibilities for the `ieee` address # The full IEEE address: ieee: 00:12:4b:00:24:42:d1:dc +``` +```yaml service: zha_toolkit.SOME_SERVICE # The short network address ieee: 0x2F3E +``` +```yaml service: zha_toolkit.SOME_SERVICE # entity name (one of them) ieee: light.tz3000_odygigth_ts0505a_12c90efe_level_light_color_on_off @@ -498,9 +503,8 @@ logging is active, this will be visible in the `home_assistant.log`. The last read this can be written to a state. ```yaml -service: zha_toolkit.execute +service: zha_toolkit.attr_write data: - command: attr_write ieee: 5c:02:72:ff:fe:92:c2:5d # The endpoint is optional - when missing tries to find endpoint matching the cluster endpoint: 11 @@ -808,10 +812,9 @@ The result is also added to the event data in the event\['data'\]\['scan'\] field ```yaml -service: zha_toolkit.execute +service: zha_toolkit.scan_device data: ieee: 00:12:4b:00:22:08:ed:1a - command: scan_device # Optional: endpoint to scan, when missing: all known endpoints # endpoint: 1 # Optional: endpoints to scan, when missing: all known endpoints @@ -821,9 +824,8 @@ data: Scan using the entity name: ```yaml -service: zha_toolkit.execute +service: zha_toolkit.scan_device data: - command: scan_device ieee: light.tz3000_odygigth_ts0505a_12c90efe_level_light_color_on_off ``` @@ -927,12 +929,10 @@ than the core that has though release procedures and is not as easily modifiable as a `custom_component`. ```yaml -service: zha_toolkit.execute +service: zha_toolkit.zcl_cmd data: # Device IEEE address - mandatory ieee: 5c:02:72:ff:fe:92:c2:5d - # Service command - mandatory - command: zcl_cmd # Command id - mandatory cmd: 0 # Cluster id - mandatory @@ -955,10 +955,9 @@ data: ### `zcl_cmd` Example: Send `on` command to an OnOff Cluster. ```yaml -service: zha_toolkit.execute +service: zha_toolkit.zcl_cmd data: ieee: 5c:02:72:ff:fe:92:c2:5d - command: zcl_cmd cmd: 1 cluster: 6 endpoint: 11 @@ -1203,9 +1202,8 @@ You can use the blueprint to setup daily backup: The name of that backup is according to the format ```yaml -service: zha_toolkit.execute +service: zha_toolkit.ezsp_backup data: - command: ezsp_backup # Optional command_data, string added to the basename. # With this example the backup is written to `nwk_backup_20220105.json` command_data: _20220105 diff --git a/STATS.md b/STATS.md index 0dfc450..e9bdac7 100644 --- a/STATS.md +++ b/STATS.md @@ -1,6 +1,7 @@ # Badges showing number of downloads per version - ![badge latest](https://img.shields.io/github/downloads/mdeweerd/zha-toolkit/latest/total.svg) +- ![badge v0.8.20](https://img.shields.io/github/downloads/mdeweerd/zha-toolkit/v0.8.20/total.svg) - ![badge v0.8.19](https://img.shields.io/github/downloads/mdeweerd/zha-toolkit/v0.8.19/total.svg) - ![badge v0.8.18](https://img.shields.io/github/downloads/mdeweerd/zha-toolkit/v0.8.18/total.svg) - ![badge v0.8.17](https://img.shields.io/github/downloads/mdeweerd/zha-toolkit/v0.8.17/total.svg) diff --git a/custom_components/zha_toolkit/binds.py b/custom_components/zha_toolkit/binds.py index f68acfa..2458719 100644 --- a/custom_components/zha_toolkit/binds.py +++ b/custom_components/zha_toolkit/binds.py @@ -39,15 +39,26 @@ async def bind_group( group_id = u.str2int(data) zdo = src_dev.zdo src_out_cls = BINDABLE_OUT_CLUSTERS + src_in_cls = BINDABLE_IN_CLUSTERS # find src ep_id dst_addr = MultiAddress() dst_addr.addrmode = t.uint8_t(1) dst_addr.nwk = t.uint16_t(group_id) + u_epid = params[p.EP_ID] + u_cluster_id = params[p.CLUSTER_ID] + + if u_cluster_id is not None: + src_out_cls = [u_cluster_id] + src_in_cls = [u_cluster_id] + results: dict[int, list[dict[str, int]]] = {} for src_out_cluster in src_out_cls: src_epid = None for ep_id, ep in src_dev.endpoints.items(): + if u_epid is not None and ep_id != u_epid: + # Endpoint not selected + continue if ep_id == 0: continue if src_out_cluster in ep.out_clusters: @@ -77,9 +88,61 @@ async def bind_group( bind_result["result"] = res results[src_epid].append(bind_result) LOGGER.debug( - "0x%04x: binding group 0x%04x: %s", src_dev.nwk, group_id, res + "0x%04x/0x%02x/0x%04x(OUT): binding group 0x%04x: %s", + src_dev.nwk, + src_epid, + src_out_cluster, + group_id, + res, ) + # find src ep_id + dst_addr = MultiAddress() + dst_addr.addrmode = t.uint8_t(1) + dst_addr.nwk = t.uint16_t(group_id) + + for src_in_cluster in src_in_cls: + src_epid = None + for ep_id, ep in src_dev.endpoints.items(): + if u_epid is not None and ep_id != u_epid: + # Endpoint not selected + continue + if ep_id == 0: + continue + if src_in_cluster in ep.in_clusters: + src_epid = ep_id + break + if not src_epid: + LOGGER.debug( + "0x%04x: skipping %s cluster (not present)", + src_dev.nwk, + src_in_cluster, + ) + continue + if src_epid not in results: + results[src_epid] = [] + LOGGER.debug( + "0x%04x: binding %s, ep: %s, cluster: %s(IN)", + src_dev.nwk, + str(src_dev.ieee), + src_epid, + src_in_cluster, + ) + bind_result = {"endpoint_id": src_epid, "cluster_id": src_in_cluster} + + res = await zdo.request( + ZDOCmd.Bind_req, src_dev.ieee, src_epid, src_in_cluster, dst_addr + ) + bind_result["result"] = res + results[src_epid].append(bind_result) + LOGGER.debug( + "0x%04x/0x%02x/0x%04x(IN): binding group 0x%04x: %s", + src_dev.nwk, + src_epid, + src_in_cluster, + group_id, + res, + ) event_data["result"] = results @@ -158,6 +221,12 @@ async def bind_ieee( src_out_clusters = BINDABLE_OUT_CLUSTERS src_in_clusters = BINDABLE_IN_CLUSTERS + u_epid = params[p.EP_ID] + u_cluster_id = params[p.CLUSTER_ID] + if u_cluster_id is not None: + src_out_clusters = [u_cluster_id] + src_in_clusters = [u_cluster_id] + # TODO: Filter according to params[p.CLUSTER_ID] results: dict[int, dict] = {} @@ -166,7 +235,9 @@ async def bind_ieee( src_endpoints = [ ep_id for ep_id, ep in src_dev.endpoints.items() - if ep_id != 0 and src_out_cluster in ep.out_clusters + if ep_id != 0 + and src_out_cluster in ep.out_clusters + and (u_epid is None or u_epid == ep_id) ] LOGGER.debug( "0x%04x: got the %s endpoints for %s cluster", @@ -226,7 +297,9 @@ async def bind_ieee( src_endpoints = [ ep_id for ep_id, ep in src_dev.endpoints.items() - if ep_id != 0 and src_in_cluster in ep.in_clusters + if ep_id != 0 + and src_in_cluster in ep.in_clusters + and (u_epid is None or u_epid == ep_id) ] LOGGER.debug( "0x%04x: got the %s endpoints for %s cluster", @@ -352,7 +425,7 @@ async def binds_remove_all( addr_mode = binding["dst"]["addrmode"] res = None - # Note, the code belowe is essentally two times the same + # Note, the code belowe is essentially two times the same # but the goal is to make a distinciont between group # and ieee addressing for testing/evolutions. if addr_mode == 1: diff --git a/custom_components/zha_toolkit/ezsp.py b/custom_components/zha_toolkit/ezsp.py index ab4c210..eeb1675 100644 --- a/custom_components/zha_toolkit/ezsp.py +++ b/custom_components/zha_toolkit/ezsp.py @@ -312,6 +312,15 @@ async def ezsp_backup_legacy( jsonfile.write(json.dumps(result, indent=4)) +async def ezsp_dummy_networkInit(): + return (bellows.types.EmberStatus.SUCCESS,) + + +async def ezsp_click_get_echo(s): + LOGGER.error(f"GET_ECHO: {s}") + bellows.cli._result = s + + async def ezsp_backup( app, listener, ieee, cmd, data, service, params, event_data ): @@ -321,11 +330,26 @@ async def ezsp_backup( raise Exception(msg) # Import stuff we need + import io import json + from contextlib import redirect_stdout from bellows.cli import backup as bellows_backup - result = await bellows_backup._backup(app._ezsp) + # from pkg_resources import parse_version + # bellows_version = bellows.__version__ + # use_click = (parse_version(bellows_version)>parse_version("0.3.1")) + + try: + # Network is already initialised, fake result for backup function + org_network_init = app._ezsp.networkInit + app._ezsp.networkInit = ezsp_dummy_networkInit + f = io.StringIO() + with redirect_stdout(f): + await bellows_backup._backup(app._ezsp) + result = f.getvalue() + finally: + app._ezsp.networkInit = org_network_init # pylint: disable=E0601 # Store backup information to file @@ -339,4 +363,4 @@ async def ezsp_backup( fname = out_dir + "nwk_backup" + str(data) + ".json" with open(fname, "w", encoding="utf_8") as jsonfile: - jsonfile.write(json.dumps(result, indent=4)) + jsonfile.write(json.dumps(json.loads(result), indent=4)) diff --git a/custom_components/zha_toolkit/services.yaml b/custom_components/zha_toolkit/services.yaml index 1db217b..44b594d 100644 --- a/custom_components/zha_toolkit/services.yaml +++ b/custom_components/zha_toolkit/services.yaml @@ -636,8 +636,84 @@ bind_ieee: selector: entity: integration: zha + endpoint: + description: 'Target endpoint (when missing: all endpoints)' + selector: + number: + min: 1 + max: 255 + mode: box + cluster: + description: Target cluster (or all internally defined ones) + selector: + number: + min: 0 + max: 0xFFFF + mode: box + tries: + description: Number of times the zigbee packet should be attempted + selector: + number: + min: 0 + max: 255 + mode: box + event_success: + description: Event name in case of success + example: my_read_success_trigger_event + selector: + text: + event_fail: + description: Event name in case of failure + example: my_read_fail_trigger_event + selector: + text: + event_done: + description: >- + Event name when the service call did all its work (either success + or failure). Has event data with relevant attributes. + example: my_read_done_trigger_event + selector: + text: + fail_exception: + description: >- + Throw exception when success==False, useful to stop scripts, automations + selector: + boolean: + expect_reply: + description: Wait for/expect a reply (not used yet) + selector: + boolean: +bind_group: + description: Bind clusters from ieee device to command_data device + fields: + ieee: + description: >- + Entity name,\ndevice name, or\nIEEE address of the node to execute + command + example: 00:0d:6f:00:05:7d:2d:34 + required: true + selector: + entity: + integration: zha + command_data: + description: >- + Target group for binding + example: 00:0d:6f:00:05:7d:2d:34 + required: true + selector: + number: + min: 0 + max: 0xFFFF + mode: box + endpoint: + description: 'Target endpoint (when missing: all endpoints)' + selector: + number: + min: 1 + max: 255 + mode: box cluster: - description: target cluster (or all internally defined ones) + description: 'Target cluster (when missing: all internally defined cluster)' selector: number: min: 0 diff --git a/custom_components/zha_toolkit/utils.py b/custom_components/zha_toolkit/utils.py index 3a84cae..b195aa9 100644 --- a/custom_components/zha_toolkit/utils.py +++ b/custom_components/zha_toolkit/utils.py @@ -257,7 +257,8 @@ async def get_ieee(app, listener, ref): device_registry = ( # Deprecated >= 2022.6.0 await listener._hass.helpers.device_registry.async_get_registry() - if packaging.version.parse(HA_VERSION) < packaging.version.parse("2022.6") + if packaging.version.parse(HA_VERSION) + < packaging.version.parse("2022.6") else listener._hass.helpers.device_registry.async_get( listener._hass ) diff --git a/setup.cfg b/setup.cfg index cd3d3e7..786cf4a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,3 +19,4 @@ max-line-length = 79 builtin=clear,rare,informal,usage,code,names ignore-words-list=hass,master skip=./.* +quiet-level=2