I have a ZigBee network for sensors controlled by an XBee S2C module with ZHA component.
I also have another XBee S2C device that controls a relay. In order to open or close the relay I must send arbitrary bytes to it from the XBee module over UART, i.e. 0x65 to open the relay and 0x6F to close it.
According to the XBee specs (https://www.digi.com/resources/documentation/digidocs/pdfs/90002002.pdf) I must issue an API command (0x10 Transmit Request frame) from the coordinator device (which is controlled by HA).
My problem is that I donāt know how to do it from HA, and even donāt know where start looking.
I guess it is not supported out-of-the-box and I probably need to write a custom module providing the service and probably customize zigpy or zha-quirks or whatever, but I need your advice on where to start, because I am not really familiar with all these stuff yet.
Ok, Iām not certain that itās possible to send arbitrary bytes through the xbee UART in API mode. It may be, but itās not something I have researched. If it is, you will likely need to build a quirk and probably add the required functionality to zigpy_xbee.
Another option would be to use the DIO pins on the xbee directly, Iām doing that now and am in the process of PRing to zhaquriks https://github.com/dmulcahey/zha-device-handlers/pull/73 This would require you to change relays, Iāve driven 3.3v SSRs directly from the DIO pins without issue.
The other option would be to add a microcontroller and have the device join as a ZHA device. You could then have the microcontroller output the required bytes. This is a project where I am using that method. https://github.com/prairiesnpr/QuadGarageDoorController Itās actually controlling 4 relays, but instead of switching a digital output, you could transmit via the UART.
Thank you!
I can see you are using remote_at_request. I believe I can use tx or tx_explicit to send UART data, without modification of zigpy_xbee.
Besides the quirks, do I need to change anything in HA itself to add a switch or service or sensor based on that quirk?
Well, Iāve tried that and it works, I can send any data on my coordinator (API mode) and the end device (xbee transparent mode) just prints it to UART. Here is a quick and dirty code that tests the idea, when I flip a switch on or off, it prints ādā or ānā: https://github.com/Shulyaka/zha-device-handlers/tree/xbee_dio2
Now, I am thinking on how to do it in a more generic way to be useful for others. Obviously, I donāt want to hardcode TOSR0X commands into xbee zhaquirk because xbee could be connected to a wide range of devices. I think of it as a device that has a service, and any module could call the service with arbitrary data. Any switches or sensors communicating to xbee could be built on top of that service.
This is where I got stuck. I donāt know how to write a zhaquirk that provides a service. Which device type should I use? Are there any other devices out there that require arbitrary data to be sent by HA?
I finally made it work as a service. One way communication so far.
Code: https://github.com/Shulyaka/zha-device-handlers/tree/serial_data_cluster
Service: zha.issue_zigbee_cluster_command
Data: {āieeeā: ā00:13:a2:00:40:f5:2a:beā, āendpoint_idā: 232, ācluster_idā: 17, ācluster_typeā: āoutā, ācommandā: 0, ācommand_typeā: āserverā, āargsā: āHello World!\nā}
Now I will try to make a switch out of this service.
To anyone looking for the solution for a similar device, here is my final configuration for TOSR08-T. It can easily be modified for TOSR04-T or TOSR02-T by removing corresponding relays or the temperature sensor if your device doesnāt have it (without the ā-Tā part). I tested it on TOSR04-T, but I will post the solution for TOSR08-T since it is easier to remove sections than to add.
Supported:
Relay control
Getting relay states from the device
Temperature sensor
Requires a recent version of zhaquircks.
homeassistant:
customize:
automation.update_tosr0x_switch:
hidden: true
automation.update_tosr0x_temp:
hidden: true
automation.update_tosr0x:
hidden: true
input_number.tosr0x_temp:
hidden: true
customize_glob:
"input_boolean.tosr0x_*_state":
hidden: true
logbook:
exclude:
entities:
- automation.update_tosr0x
- automation.update_tosr0x_temp
- automation.update_tosr0x_switch
# Internal hidden variables to hold TOSR0X relay state
input_boolean:
tosr0x_1_state:
name: TOSR0X Relay 1 State
tosr0x_2_state:
name: TOSR0X Relay 2 State
tosr0x_3_state:
name: TOSR0X Relay 3 State
tosr0x_4_state:
name: TOSR0X Relay 4 State
tosr0x_5_state:
name: TOSR0X Relay 5 State
tosr0x_6_state:
name: TOSR0X Relay 6 State
tosr0x_7_state:
name: TOSR0X Relay 7 State
tosr0x_8_state:
name: TOSR0X Relay 8 State
# Internal variable to hold TOSR0X-T temperature
input_number:
tosr0x_temp:
name: TOSR0X-T Temperature State
min: -55
max: 125
step: 0.125
mode: box
unit_of_measurement: 'Ā°C'
icon: mdi:temperature-celsius
# Sensor to display the TOSR0X-T temperature
sensor:
- platform: template
sensors:
tosr0x_temperature:
friendly_name: TOSR0X-T Temperature
entity_id: input_number.tosr0x_temp
value_template: "{{ states.input_number.tosr0x_temp.state }}"
device_class: temperature
unit_of_measurement: 'Ā°C'
# Switches to represent TOSR0X relays
# http://www.tinyosshop.com/datasheet/TOSR0X-T%20User%20Manual.pdf
switch:
- platform: template
switches:
tosr0x_1:
friendly_name: TOSR0X Relay 1
value_template: "{{ is_state('input_boolean.tosr0x_1_state', 'on') }}"
turn_on:
- service: zha.issue_zigbee_cluster_command
data:
ieee: 00:13:a2:00:40:f5:2a:be
endpoint_id: 232
cluster_id: 17
cluster_type: in
command: 0
command_type: server
args: "e[" # First character is to control the switch. Second character ('[') is to request an update of the switch states from the device in order to double confirm
- service: input_boolean.turn_on # Update switch state instantly without waiting for the update from the device (aka optimistic mode)
data:
entity_id: input_boolean.tosr0x_1_state
turn_off:
- service: zha.issue_zigbee_cluster_command
data:
ieee: 00:13:a2:00:40:f5:2a:be
endpoint_id: 232
cluster_id: 17
cluster_type: in
command: 0
command_type: server
args: "o["
- service: input_boolean.turn_off
data:
entity_id: input_boolean.tosr0x_1_state
tosr0x_2:
friendly_name: TOSR0X Relay 2
value_template: "{{ is_state('input_boolean.tosr0x_2_state', 'on') }}"
turn_on:
- service: zha.issue_zigbee_cluster_command
data:
ieee: 00:13:a2:00:40:f5:2a:be
endpoint_id: 232
cluster_id: 17
cluster_type: in
command: 0
command_type: server
args: "f["
- service: input_boolean.turn_on
data:
entity_id: input_boolean.tosr0x_2_state
turn_off:
- service: zha.issue_zigbee_cluster_command
data:
ieee: 00:13:a2:00:40:f5:2a:be
endpoint_id: 232
cluster_id: 17
cluster_type: in
command: 0
command_type: server
args: "p["
- service: input_boolean.turn_off
data:
entity_id: input_boolean.tosr0x_2_state
tosr0x_3:
friendly_name: TOSR0X Relay 3
value_template: "{{ is_state('input_boolean.tosr0x_3_state', 'on') }}"
turn_on:
- service: zha.issue_zigbee_cluster_command
data:
ieee: 00:13:a2:00:40:f5:2a:be
endpoint_id: 232
cluster_id: 17
cluster_type: in
command: 0
command_type: server
args: "g["
- service: input_boolean.turn_on
data:
entity_id: input_boolean.tosr0x_3_state
turn_off:
- service: zha.issue_zigbee_cluster_command
data:
ieee: 00:13:a2:00:40:f5:2a:be
endpoint_id: 232
cluster_id: 17
cluster_type: in
command: 0
command_type: server
args: "q["
- service: input_boolean.turn_off
data:
entity_id: input_boolean.tosr0x_3_state
tosr0x_4:
friendly_name: TOSR0X Relay 4
value_template: "{{ is_state('input_boolean.tosr0x_4_state', 'on') }}"
turn_on:
- service: zha.issue_zigbee_cluster_command
data:
ieee: 00:13:a2:00:40:f5:2a:be
endpoint_id: 232
cluster_id: 17
cluster_type: in
command: 0
command_type: server
args: "h["
- service: input_boolean.turn_on
data:
entity_id: input_boolean.tosr0x_4_state
turn_off:
- service: zha.issue_zigbee_cluster_command
data:
ieee: 00:13:a2:00:40:f5:2a:be
endpoint_id: 232
cluster_id: 17
cluster_type: in
command: 0
command_type: server
args: "r["
- service: input_boolean.turn_off
data:
entity_id: input_boolean.tosr0x_4_state
tosr0x_5:
value_template: "{{ is_state('input_boolean.tosr0x_5_state', 'on') }}"
turn_on:
- service: zha.issue_zigbee_cluster_command
data:
ieee: 00:13:a2:00:40:f5:2a:be
endpoint_id: 232
cluster_id: 17
cluster_type: out
command: 0
command_type: server
args: "i["
- service: input_boolean.turn_on
data:
entity_id: input_boolean.tosr0x_5_state
turn_off:
- service: zha.issue_zigbee_cluster_command
data:
ieee: 00:13:a2:00:40:f5:2a:be
endpoint_id: 232
cluster_id: 17
cluster_type: out
command: 0
command_type: server
args: "s["
- service: input_boolean.turn_off
data:
entity_id: input_boolean.tosr0x_5_state
tosr0x_6:
value_template: "{{ is_state('input_boolean.tosr0x_6_state', 'on') }}"
turn_on:
- service: zha.issue_zigbee_cluster_command
data:
ieee: 00:13:a2:00:40:f5:2a:be
endpoint_id: 232
cluster_id: 17
cluster_type: out
command: 0
command_type: server
args: "j["
- service: input_boolean.turn_on
data:
entity_id: input_boolean.tosr0x_6_state
turn_off:
- service: zha.issue_zigbee_cluster_command
data:
ieee: 00:13:a2:00:40:f5:2a:be
endpoint_id: 232
cluster_id: 17
cluster_type: out
command: 0
command_type: server
args: "t["
- service: input_boolean.turn_off
data:
entity_id: input_boolean.tosr0x_6_state
tosr0x_7:
value_template: "{{ is_state('input_boolean.tosr0x_7_state', 'on') }}"
turn_on:
- service: zha.issue_zigbee_cluster_command
data:
ieee: 00:13:a2:00:40:f5:2a:be
endpoint_id: 232
cluster_id: 17
cluster_type: out
command: 0
command_type: server
args: "k["
- service: input_boolean.turn_on
data:
entity_id: input_boolean.tosr0x_7_state
turn_off:
- service: zha.issue_zigbee_cluster_command
data:
ieee: 00:13:a2:00:40:f5:2a:be
endpoint_id: 232
cluster_id: 17
cluster_type: out
command: 0
command_type: server
args: "u["
- service: input_boolean.turn_off
data:
entity_id: input_boolean.tosr0x_7_state
tosr0x_8:
value_template: "{{ is_state('input_boolean.tosr0x_8_state', 'on') }}"
turn_on:
- service: zha.issue_zigbee_cluster_command
data:
ieee: 00:13:a2:00:40:f5:2a:be
endpoint_id: 232
cluster_id: 17
cluster_type: out
command: 0
command_type: server
args: "l["
- service: input_boolean.turn_on
data:
entity_id: input_boolean.tosr0x_8_state
turn_off:
- service: zha.issue_zigbee_cluster_command
data:
ieee: 00:13:a2:00:40:f5:2a:be
endpoint_id: 232
cluster_id: 17
cluster_type: out
command: 0
command_type: server
args: "v["
- service: input_boolean.turn_off
data:
entity_id: input_boolean.tosr0x_8_state
automation:
- alias: update_tosr0x # Request the temperature and relay states from TOSR0X
trigger:
- platform: time_pattern
minutes: '/5'
- platform: homeassistant
event: start
action:
service: zha.issue_zigbee_cluster_command
data:
ieee: 00:13:a2:00:40:f5:2a:be
endpoint_id: 232
cluster_id: 17
cluster_type: in
command: 0
command_type: server
args: "a[" # First character is for temperature, second character is for relay states, please see the TOSR0X manual
- alias: update_tosr0x_switch # Update relay states upon receiving them from TOSR0X
trigger:
platform: event
event_type: zha_event
event_data:
device_ieee: 00:13:a2:00:40:f5:2a:be
command: receive_data
condition:
condition: template
value_template: "{{ trigger.event.data.args | length == 1 }}" # A single byte response is probably a relay state update
action:
- service_template: >
{% if trigger.event.data.args.encode('latin1').hex() | int(base=16) | bitwise_and(2**0) %}
input_boolean.turn_on
{% else %}
input_boolean.turn_off
{% endif %}
data:
entity_id: input_boolean.tosr0x_1_state
- service_template: >
{% if trigger.event.data.args.encode('latin1').hex() | int(base=16) | bitwise_and(2**1) %}
input_boolean.turn_on
{% else %}
input_boolean.turn_off
{% endif %}
data:
entity_id: input_boolean.tosr0x_2_state
- service_template: >
{% if trigger.event.data.args.encode('latin1').hex() | int(base=16) | bitwise_and(2**2) %}
input_boolean.turn_on
{% else %}
input_boolean.turn_off
{% endif %}
data:
entity_id: input_boolean.tosr0x_3_state
- service_template: >
{% if trigger.event.data.args.encode('latin1').hex() | int(base=16) | bitwise_and(2**3) %}
input_boolean.turn_on
{% else %}
input_boolean.turn_off
{% endif %}
data:
entity_id: input_boolean.tosr0x_4_state
- service_template: >
{% if trigger.event.data.args.encode('latin1').hex() | int(base=16) | bitwise_and(2**4) %}
input_boolean.turn_on
{% else %}
input_boolean.turn_off
{% endif %}
data:
entity_id: input_boolean.tosr0x_5_state
- service_template: >
{% if trigger.event.data.args.encode('latin1').hex() | int(base=16) | bitwise_and(2**5) %}
input_boolean.turn_on
{% else %}
input_boolean.turn_off
{% endif %}
data:
entity_id: input_boolean.tosr0x_6_state
- service_template: >
{% if trigger.event.data.args.encode('latin1').hex() | int(base=16) | bitwise_and(2**6) %}
input_boolean.turn_on
{% else %}
input_boolean.turn_off
{% endif %}
data:
entity_id: input_boolean.tosr0x_7_state
- service_template: >
{% if trigger.event.data.args.encode('latin1').hex() | int(base=16) | bitwise_and(2**7) %}
input_boolean.turn_on
{% else %}
input_boolean.turn_off
{% endif %}
data:
entity_id: input_boolean.tosr0x_8_state
- alias: update_tosr0x_temp # Update temperature upon receiving it from TOSR0X
trigger:
platform: event
event_type: zha_event
event_data:
device_ieee: 00:13:a2:00:40:f5:2a:be
command: receive_data
condition:
condition: template
value_template: "{{ trigger.event.data.args | length == 2 }}" # A two-byte response is probably a temperature update (2's complement format)
action:
service: input_number.set_value
data_template:
entity_id: input_number.tosr0x_temp
value: >
{% if trigger.event.data.args.encode('latin1').hex() | int(base=16) > 32767 %}
{{ trigger.event.data.args.encode('latin1').hex() | int(base=16) / 16 - 4096 }}
{% else %}
{{ trigger.event.data.args.encode('latin1').hex() | int(base=16) / 16 }}
{% endif %}
group:
tosr0x:
name: TOSR0X
entities:
- zha.xbee_xbee_io
- sensor.tosr0x_temperature
- switch.tosr0x_1
- switch.tosr0x_2
- switch.tosr0x_3
- switch.tosr0x_4
- switch.tosr0x_5
- switch.tosr0x_6
- switch.tosr0x_7
- switch.tosr0x_8
Probably you could just enable join in HA interface (http://hassio.local:8123/config/zha/add) and press the commissioning button on the second xbee (pin 20 to ground) once.
If that doesnāt work, place the two xbees closer to each other.
Instead of commisioning button you can try XTCU to simulate it or to manually set the parameters. The important are:
CE=0
ZS=2
EE=1
EO=2
NK=0
KY=5a6967426565416c6c69616e63653039
It is the default trusted center (usually, the coordinator node) link key documented in zigbee standard. It is used during join to transfer the actual network key, which is randomly generated by the trust center node. It is not used by nodes when they already joined.
You can. Please note however:
The key must be preconfigured on all new nodes joining the network. If all your devices are xbee, then it is not a problem. For less configurable devices such as sensors it could be an issue.
Currently the link key is hardcoded in the zigpy_xbee code. If you overwrite it, you will need to do it after every zigpy_xbee update.
Thanks, this thread has really helped me. One thing:
Note that the github projects referenced from this and the previous posts have been removed. It took me a while to eventually figure out that you had integrated your work into https://github.com/dmulcahey/zha-device-handlers/
Yes, my changes were accepted upstream, now you donāt need anything beyond a recent version of zhaquircks. I probably needed to mention that more explicitly in the post that I marked as a solution.
Thanks for a great solution
I struggled to come up with a solution for interfacing from Home Assistant to micropython on XBee3 until I stumbled on this.
I used a similar configuration to control XBee3 micropython which activates Seeed SPDT relays over i2c.
The configuration looks like this - I have just shown one of the four relays:
The micropython on the XBee3 looks as follows (The XBee3 needs to be configured in API mode):
import time
import xbee
from machine import I2C
import ujson
import struct
i2c = I2C(1)
CMD_CHANNEL_CTRL = 0x10
relayState = 0x0
while True:
# Check if the XBee has any message in the queue.
received_msg = xbee.receive()
if received_msg:
# Get the sender's 64-bit address and payload from the received message.
sender = received_msg['sender_eui64']
payload = received_msg['payload']
try:
obj = ujson.loads(payload.decode())
if obj['relay'] < 1 | obj['relay'] > 8:
print("relay must be an integer from 1 - 8")
else:
if obj['status']:
relayState |= (1 << obj['relay'] - 1)
else:
relayState &= ~(1 << obj['relay'] - 1)
cmdBytes = struct.pack("BB", CMD_CHANNEL_CTRL, relayState)
print(relayState)
print(cmdBytes)
i2c.writeto(17, cmdBytes)
# Send back the same payload to the sender.
print("Sending back a response...\n")
# xbee.transmit(sender, payload)
except ValueError:
print("Failed to parse json for payload:")
print(payload)
else:
# Wait 100 ms before checking for data again.
time.sleep(0.2)
I would like to look at using more standard Zigbee protocol message to drive the micropython, but for now I am pleased with the progress.