Skip to content

Commit

Permalink
Merge pull request #13 from mdeweerd/dev
Browse files Browse the repository at this point in the history
- Extend documentation (service screenshot);
- Extend services.yaml (UI parameters);
- Add 'fail_exception' to generate service exception on "failure" (e.g. Zigbee command succeeds, but returns success false, or read back is different from write);
- scan_device filename improved: better partial IEEE address, include selected endpoint (list);
- sort zha_device list according to 'csvlabel' - cope with absent values for sorting.
  • Loading branch information
mdeweerd authored Feb 10, 2022
2 parents 05422ef + 38f9d92 commit ab2b9c3
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 12 deletions.
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ data:
zigpy.zcl: debug
```

For sleepy devices (on a battery) you may need to wake them up
just after sending the command so that they can receive it.


# Automations

This is a list (of 1) automation:
Expand Down Expand Up @@ -106,19 +110,24 @@ Feel free to propose documentation updates.

# Examples

Examples are work in progress and may not be functional.
Services are easy to called once or tested through Developer Tools > Services . And you can also use them in scripts, automations, etc. .

Quite a few services can be configured from the UI. And you can also start using the UI (to select the ieee/entity for instance), and then Go To YAML mode to add the other parameters.

Empty UI example:
![image](images/service-config-ui.png)

For sleepy devices (on a battery) you may need to wake them up
just after sending the command so that they can receive it.

The 'ieee' address can be the IEEE address, the short network address
(0x1203 for instance), or the entity name (example: "light.tz3000_odygigth_ts0505a_12c90efe_level_light_color_on_off"). Be aware that the network address can change over
time but it is shorter to enter if you know it.


All commands support setting event names.
When set, These events are generated at the end of the command execution.

For sleepy devices (on a battery) the "tries" option can be useful to try many times until the command succeeds.
Or, you may need to wake them up just after sending the command so that they can receive it.


```yaml
# You can set the next events to use as a trigger.
Expand Down Expand Up @@ -669,10 +678,14 @@ data:
## `zha_devices`: Get information about devices in network to CSV

Write information from currently known ZHA devices to a CSV file.
You also get this data in the 'devices' field of the generated events which
allows you to get information about endpoints and services as well.

```yaml
service: zha_toolkit.zha_devices
data:
# Optional list of fields to write to the CSV, all non-list fields by default.
command_data: ['name', 'ieee', 'rssi', 'lqi']
csvout: ../www/devices.csv
event_done: zha_devices
Expand Down
9 changes: 7 additions & 2 deletions custom_components/zha_toolkit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,12 @@
vol.Optional(P.EVENT_SUCCESS): cv.string,
vol.Optional(P.EVENT_FAIL): cv.string,
vol.Optional(P.EVENT_DONE): cv.string,
vol.Optional(P.EXPECT_REPLY): cv.boolean, # May be called 'wait' later?
vol.Optional(
P.FAIL_EXCEPTION
): cv.boolean, # raise exception when success==False
vol.Optional(
P.EXPECT_REPLY
): cv.boolean, # To be use where Zigpy uses 'expect_reply'
}


Expand Down Expand Up @@ -492,7 +497,7 @@ async def toolkit_service(service):
if handler_exception is not None:
raise handler_exception

if not event_data["success"] and params[p.EXPECT_REPLY]:
if not event_data["success"] and params[p.FAIL_EXCEPTION]:
raise Exception("Success expected, but failed")

# Set up all service schemas
Expand Down
2 changes: 2 additions & 0 deletions custom_components/zha_toolkit/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class USER_PARAMS_consts:
EVENT_SUCCESS = "event_success"
EVENT_FAIL = "event_fail"
EVENT_DONE = "event_done"
FAIL_EXCEPTION = "fail_exception"
READ_BEFORE_WRITE = "read_before_write"
READ_AFTER_WRITE = "read_after_write"
WRITE_IF_EQUAL = "write_if_equal"
Expand Down Expand Up @@ -105,6 +106,7 @@ class INTERNAL_PARAMS_consts:
EVT_FAIL = "event_fail"
EVT_SUCCESS = "event_success"
EXPECT_REPLY = "expect_reply"
FAIL_EXCEPTION = "fail_exception"
MANF = "manf"
MAX_INTERVAL = "max_interval"
MIN_INTERVAL = "min_interval"
Expand Down
37 changes: 31 additions & 6 deletions custom_components/zha_toolkit/scan_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ async def scan_results(device, endpoints=None):
if endpoints is not None and isinstance(endpoints, int):
endpoints = [endpoints]

if endpoints is None or not isinstance(endpoints, list):
if (
endpoints is None
or not isinstance(endpoints, list)
or len(endpoints) == 0
):
endpoints = []
for epid, _ep in device.endpoints.items():
endpoints.append(epid)
Expand Down Expand Up @@ -323,18 +327,39 @@ async def scan_device(

device = app.get_device(ieee)

scan = await scan_results(device, params[p.EP_ID])
endpoints = params[p.EP_ID]

if endpoints is None:
endpoints = []
elif isinstance(endpoints, int):
endpoints = [endpoints]
elif not isinstance(endpoints, list):
raise ValueError("Endpoint must be int or list of int")

endpoints = sorted(set(endpoints)) # Uniqify and sort

scan = await scan_results(device, endpoints)

event_data["scan"] = scan

model = scan.get("model")
manufacturer = scan.get("manufacturer")

if len(endpoints) == 0:
ep_str = ""
else:
ep_str = "_" + ("_".join([f"{e:02x}" for e in endpoints]))

postfix = f"{ep_str}_scan_results.txt"

# Set a unique filename for each device, using the manf name and
# the variable part of the device mac address
if model is not None and manufacturer is not None:
ieee_tail = "".join([f"{o:02x}" for o in ieee[-4:]])
file_name = f"{model}_{manufacturer}_{ieee_tail}_scan_results.txt"
ieee_tail = "".join([f"{o:02x}" for o in ieee[4::-1]])
file_name = f"{model}_{manufacturer}_{ieee_tail}{postfix}"
else:
ieee_tail = "".join([f"{o:02x}" for o in ieee])
file_name = f"{ieee_tail}_scan_results.txt"
ieee_tail = "".join([f"{o:02x}" for o in ieee[::-1]])
file_name = f"{ieee_tail}{postfix}"

u.write_json_to_file(
scan,
Expand Down
86 changes: 86 additions & 0 deletions custom_components/zha_toolkit/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ execute:
example: my_read_done_trigger_event
selector:
text:
fail_exception:
description: "Throw exception when success==False, useful to stop scripts, automations"
selector:
boolean:
allow_create:
description: Allow state creation (given by state_id) if it does not exist
selector:
Expand Down Expand Up @@ -275,6 +279,10 @@ attr_read:
example: my_read_done_trigger_event
selector:
text:
fail_exception:
description: "Throw exception when success==False, useful to stop scripts, automations"
selector:
boolean:
allow_create:
description: Allow state creation (given by state_id) if it does not exist
selector:
Expand Down Expand Up @@ -377,6 +385,10 @@ attr_write:
example: my_read_done_trigger_event
selector:
text:
fail_exception:
description: "Throw exception when success==False, useful to stop scripts, automations"
selector:
boolean:
allow_create:
description: Allow state creation (given by state_id) if it does not exist
selector:
Expand Down Expand Up @@ -430,6 +442,10 @@ backup:
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:
Expand Down Expand Up @@ -480,6 +496,10 @@ bind_ieee:
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:
Expand Down Expand Up @@ -570,6 +590,10 @@ conf_report:
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:
Expand Down Expand Up @@ -597,6 +621,10 @@ ezsp_backup:
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:
Expand Down Expand Up @@ -643,6 +671,10 @@ ieee_ping:
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:
Expand All @@ -669,6 +701,14 @@ scan_device:
selector:
entity:
integration: zha
endpoint:
description: Target endpoint, or list of endpoints
example: 1
selector:
number:
min: 1
max: 255
mode: box
event_success:
description: Event name in case of success
example: my_read_success_trigger_event
Expand All @@ -684,6 +724,10 @@ scan_device:
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:
Expand Down Expand Up @@ -711,7 +755,49 @@ znp_backup:
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:
zha_devices:
description: Export IEEE device information to CSV file or Event
fields:
command_data:
description: List of columns for csv file
example: ["ieee", "lqi", "name" ]
selector:
text:
csvout:
description: Filename of CSV to write read data to. Written to 'csv' directory (can be relative as in example).
example: '../web/mycsv.csv'
selector:
text:
csvlabel:
description: Column to sort table by
example: "lqi"
selector:
select:
options: [ "ieee", "nwk", "manufacturer", "model", "name", "quirk_applied", "quirk_class", "manufacturer_code", "power_source", "lqi", "rssi", "last_seen", "available", "device_type", "user_given_name", "device_reg_id", "area_id", ]
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:
4 changes: 4 additions & 0 deletions custom_components/zha_toolkit/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ def extractParams(service): # noqa: C901
p.EVT_SUCCESS: None,
p.EVT_FAIL: None,
p.EVT_DONE: None,
p.FAIL_EXCEPTION: False,
p.READ_BEFORE_WRITE: True,
p.READ_AFTER_WRITE: True,
p.WRITE_IF_EQUAL: False,
Expand Down Expand Up @@ -433,6 +434,9 @@ def extractParams(service): # noqa: C901
if P.EXPECT_REPLY in rawParams:
params[p.EXPECT_REPLY] = str2int(rawParams[P.EXPECT_REPLY]) == 0

if P.FAIL_EXCEPTION in rawParams:
params[p.FAIL_EXCEPTION] = str2int(rawParams[P.FAIL_EXCEPTION]) == 0

if P.ARGS in rawParams:
cmd_args = []
for val in rawParams[P.ARGS]:
Expand Down
13 changes: 13 additions & 0 deletions custom_components/zha_toolkit/zha.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ async def zha_devices(
devices = [device.zha_device_info for device in listener.devices.values()]
event_data["devices"] = devices

if params[p.CSV_LABEL] is not None and type(params[p.CSV_LABEL]) == str:
try:
# Lamba function gets column and returns false if None
# This make compares possible for ints)
devices = sorted(
devices,
key=lambda item: (lambda a: (a is None, a))(
item[params[p.CSV_LABEL]]
),
)
except Exception: # nosec
pass

if params[p.CSV_FILE] is not None:
if data is not None and type(data) == list:
columns = data
Expand Down
Binary file added images/service-config-ui.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit ab2b9c3

Please sign in to comment.