One of the challenges faced with creating a stable zigbee installation is potential channel interference in the crowded 2.4GHz band.
There is the often reference Metageek Zigbee-Wifi coexistence article which is helpful in understanding where channels sit within that band. If you have ZHA installed there is also the option to perform an energy-scan to get a rudimentary view of RF energy within each zigbee channel. This is useful but is just a point in time snapshot. There is also the zigpy-cli utility which enables you to run the same energy-scan from the command line.
A much better understanding of channel interference can be gained if the 2.4GHz band could be continuously monitored. This is what this project attempts to achieve.
This is a bit hacky and has a number of issues to be aware of but the end result looks like this.
Prerequisites.
A spare zigbee co-ordinator. I am using a SONOFF Zigbee 3.0 USB Dongle Plus-E but a conbee II or CC2652 based co-ordinator will be fine, basically anything that zigpy supports.
Overview.
The process involves the following.
-
Install ZHA to create a zigbee network.
-
A shell script that runs zigpy to perform an energy-scan. This is passed into awk to format the output as json
-
A template sensor that executes the shell script and stores the return values for each zigbee channel as an attribute.
-
For ease of graphing a template sensor for each channel is then created.
-
Each channel is then displayed using bar-card
Hereโs the hacky bit. Shell Command runs within the homeassistant
container context. That means the zigpy package needs to be added to this container. It also means that when Home Assistant is updated the zigpy package needs to be re-installed.
Detail.
zigpy installation.
Plug in your spare coordinator and check it appears on your system.
To perform an energy-scan you have to create a zigbee network. I used ZHA for this as I use zigbee2mqtt for my actual zigbee network. You should be able to use Z2M for this but I have not tested this.
Install ZHA, specifying the spare coordinator. Once installed you should disable ZHA as it is no longer required and if it is running zigpy will not be able to connect to the coordinator.
Open a terminal windows on your HA server and create the following file called energy_scan.sh in the config directory -
zigpy radio ezsp /dev/serial/by-id/usb-ITEAD_SONOFF_Zigbee_3.0_USB_Dongle_Plus_V2_20230508163412-if00 energy-scan -n 0 | \
awk ' BEGIN { print "{" } \
( NR > 11 && NR < 28) { gsub(/[^0-9. ]/ ,"") ; print "\x22"$1"\x22" ": " , "\x22"$2"\x22" "," } \
END { print "\x22"0"\x22" ": " "\x22"0"\x22" "}" }'
Edit the script to select the correct type of coordinator (ezsp|deconz|xbee|zigate|znp) and the correct reference in /dev/serial/by-id
Ensure the script is executable by executing the command chmod a+x energy_scan.sh
To test this within the terminal window you have to install zigpy as follows -
pip install zigpy-cli
Running the script produces the following json formatted output (Your numbers will be different) -
{
"11": "15.32",
"12": "75.96",
"13": "73.51",
"14": "13.71",
"15": "88.70",
"16": "43.06",
"17": "65.26",
"18": "36.83",
"19": "28.30",
"20": "13.71",
"21": "4.15",
"22": "4.15",
"23": "4.15",
"24": "4.15",
"25": "4.15",
"26": "87.33",
"0": "0"}
This is a channel-power pair showing each channel and the RF energy detected. This is described as a %utilisation but I have no idea how this is derived. However for our purposes it is just a number for comparing different channels.
Be aware, If you run this script after you have set all this up it will fail because it will not be able to get exclusive access to your spare zigbee coordinator.
Next, whilst in the terminal window, we need to install zigpy in the homeassistant
container. Type the following -
docker exec -it homeassistant bash This will log you into the homeassistant
docker container.
pip install zigpy-cli which installs the zigpy package in the homeassistant
which is where our shell command (energy_scan.sh) will run from.
YOU WILL HAVE TO REDO THIS EACH TIME YOU UPGRADE HOMEASSISTANT
As before, execute energy_scan.sh in the config directory to check zigpy is installed and working in the homeassistant
container.
exit out of the homeassistant
container. That completes the setup of zigpy.
Home Assistant configuration.
Edit your configuration.yaml file and add the following -
shell_command:
energy_scan: bash energy_scan.sh
After restarting Home Assistant this will create a service in Home Assistant called Shell Command: energy_scan
You can execute this service from the Developer Tools to check it is producing the same output as running energy_scan.sh from the terminal window.
Be aware, If you call this service after you have set all this up it will fail because it will not be able to get exclusive access to your spare zigbee coordinator.
We now need to create some template sensors. Add the following to your configuration.yaml file -
template:
- trigger:
- platform: homeassistant
event: start
- platform: time_pattern
seconds: /10
action:
- service: shell_command.energy_scan
data: {}
response_variable: return_response
sensor:
- name: Zigbee Channels
unique_id: zigbee_channels
state: "{{ return_response['returncode'] }}"
attributes:
11: >
{% set channels = (return_response['stdout'] | from_json) %}
{{ channels['11'] }}
12: >
{% set channels = (return_response['stdout'] | from_json) %}
{{ channels['12'] }}
13: >
{% set channels = (return_response['stdout'] | from_json) %}
{{ channels['13'] }}
14: >
{% set channels = (return_response['stdout'] | from_json) %}
{{ channels['14'] }}
15: >
{% set channels = (return_response['stdout'] | from_json) %}
{{ channels['15'] }}
16: >
{% set channels = (return_response['stdout'] | from_json) %}
{{ channels['16'] }}
17: >
{% set channels = (return_response['stdout'] | from_json) %}
{{ channels['17'] }}
18: >
{% set channels = (return_response['stdout'] | from_json) %}
{{ channels['18'] }}
19: >
{% set channels = (return_response['stdout'] | from_json) %}
{{ channels['19'] }}
20: >
{% set channels = (return_response['stdout'] | from_json) %}
{{ channels['20'] }}
21: >
{% set channels = (return_response['stdout'] | from_json) %}
{{ channels['21'] }}
22: >
{% set channels = (return_response['stdout'] | from_json) %}
{{ channels['22'] }}
23: >
{% set channels = (return_response['stdout'] | from_json) %}
{{ channels['23'] }}
24: >
{% set channels = (return_response['stdout'] | from_json) %}
{{ channels['24'] }}
25: >
{% set channels = (return_response['stdout'] | from_json) %}
{{ channels['25'] }}
26: >
{% set channels = (return_response['stdout'] | from_json) %}
{{ channels['26'] }}
- sensor:
- name: Zigbee Channel 11
unit_of_measurement: "%"
state_class: measurement
unique_id: zigbee_channel_11
state: "{{ state_attr('sensor.zigbee_channels','11') }}"
- name: Zigbee Channel 12
unit_of_measurement: "%"
state_class: measurement
unique_id: zigbee_channel_12
state: "{{ state_attr('sensor.zigbee_channels','12') }}"
- name: Zigbee Channel 13
unit_of_measurement: "%"
state_class: measurement
unique_id: zigbee_channel_13
state: "{{ state_attr('sensor.zigbee_channels','13') }}"
- name: Zigbee Channel 14
unit_of_measurement: "%"
state_class: measurement
unique_id: zigbee_channel_14
state: "{{ state_attr('sensor.zigbee_channels','14') }}"
- name: Zigbee Channel 15
unit_of_measurement: "%"
state_class: measurement
unique_id: zigbee_channel_15
state: "{{ state_attr('sensor.zigbee_channels','15') }}"
- name: Zigbee Channel 16
unit_of_measurement: "%"
state_class: measurement
unique_id: zigbee_channel_16
state: "{{ state_attr('sensor.zigbee_channels','16') }}"
- name: Zigbee Channel 17
unit_of_measurement: "%"
state_class: measurement
unique_id: zigbee_channel_17
state: "{{ state_attr('sensor.zigbee_channels','17') }}"
- name: Zigbee Channel 18
unit_of_measurement: "%"
state_class: measurement
unique_id: zigbee_channel_18
state: "{{ state_attr('sensor.zigbee_channels','18') }}"
- name: Zigbee Channel 19
unit_of_measurement: "%"
state_class: measurement
unique_id: zigbee_channel_19
state: "{{ state_attr('sensor.zigbee_channels','19') }}"
- name: Zigbee Channel 20
unit_of_measurement: "%"
state_class: measurement
unique_id: zigbee_channel_20
state: "{{ state_attr('sensor.zigbee_channels','20') }}"
- name: Zigbee Channel 21
unit_of_measurement: "%"
state_class: measurement
unique_id: zigbee_channel_21
state: "{{ state_attr('sensor.zigbee_channels','21') }}"
- name: Zigbee Channel 22
unit_of_measurement: "%"
state_class: measurement
unique_id: zigbee_channel_22
state: "{{ state_attr('sensor.zigbee_channels','22') }}"
- name: Zigbee Channel 23
unit_of_measurement: "%"
state_class: measurement
unique_id: zigbee_channel_23
state: "{{ state_attr('sensor.zigbee_channels','23') }}"
- name: Zigbee Channel 24
unit_of_measurement: "%"
state_class: measurement
unique_id: zigbee_channel_24
state: "{{ state_attr('sensor.zigbee_channels','24') }}"
- name: Zigbee Channel 25
unit_of_measurement: "%"
state_class: measurement
unique_id: zigbee_channel_25
state: "{{ state_attr('sensor.zigbee_channels','25') }}"
- name: Zigbee Channel 26
unit_of_measurement: "%"
state_class: measurement
unique_id: zigbee_channel_26
state: "{{ state_attr('sensor.zigbee_channels','26') }}"
This will create a template sensor called sensor.zigbee_channels which will store each channel power % as an attribute. It calls shell_command: energy_scan every 10 seconds which is the fastest update you can achieve. The extra load on my RPi4 was ~5%
This is then broken out into individual sensors called sensor.zigbee_channel_11 to sensor.zigbee_channel_26. I chose to do this as it makes examining sensor history and displaying the output easier.
Now I am sure there is a better way to extract the channel information in the template sensors but I will leave that for a jinja ninja to comment on.
I implemented the execution of the shell_command service as a template trigger sensor but it may be better as an automation because it can be disabled if not required. You would however lose sensor history in that case.
Display.
To display the output I used the excellent bar card to show each channel as a vertical bar.
Create a new card in and add the following -
entities:
- entity: sensor.zigbee_channel_11
name: '11'
- entity: sensor.zigbee_channel_12
name: '12'
- entity: sensor.zigbee_channel_13
name: '13'
- entity: sensor.zigbee_channel_14
name: '14'
- entity: sensor.zigbee_channel_15
name: '15'
- entity: sensor.zigbee_channel_16
name: '16'
- entity: sensor.zigbee_channel_17
name: '17'
- entity: sensor.zigbee_channel_18
name: '18'
- entity: sensor.zigbee_channel_19
name: '19'
- entity: sensor.zigbee_channel_20
name: '20'
- entity: sensor.zigbee_channel_21
name: '21'
- entity: sensor.zigbee_channel_22
name: '22'
- entity: sensor.zigbee_channel_23
name: '23'
- entity: sensor.zigbee_channel_24
name: '24'
- entity: sensor.zigbee_channel_25
name: '25'
- entity: sensor.zigbee_channel_26
name: '26'
title: Zigbee Channel Utilisation
direction: up
height: 200px
stack: horizontal
type: custom:bar-card
This produces the bar graph shown before that gives you readings for each zigbee channel updated every 10 seconds. You can access each channels historical data by clicking on the icon at the bottom of each channel.
As I mentioned this is a bit hacky and I am sure it can be improved on. It feels like it could be converted to an add-on but thatโs not a skill I currently have.
~Brian