With the recent release of the affordable Sonoff Zigbee range, I decided to upgrade some sensors around the house from RF to Zigbee. Here is how I added them to my openHAB installation.
There are (at least) two ways to go about adding Zigbee things:
- The Zigbee Binding
- Using a Zigbee to MQTT bridge
I chose the second option.
Prerequisites:
This guide assumes that you already have a working MQTT Broker such as the embedded openHAB MQTT Broker Moquette or Mosquitto.
Note: This guide contains references to a Bridge to the embedded MQTT broker (mqtt:broker:bridge). Replace that with a reference to your own broker bridge, where necessary.
ZigBee Hardware
- CC2531: IEEE 802.15.4 wireless MCU
- SNZB-01: Wireless Switch
- SNZB-02: Temperature and Humidity Sensor*
- SNZB-03: Motion Sensor
- SNZB-04: Wireless door/window sensor
Note: * = Not covered yet as I donât have one.
Software
- Zigbee2MQTT: Zigbee to MQTT bridge
Add-ons
- JSONPath Transformation: Extracts values from a JSON string using a JsonPath expression
- Map Transformation: Transforms the input by mapping it to another string.
- Regex Transformation: Transforms a source string on basis of the regular expression (regex) search pattern to a defined result string
Procedure:
-
Prepare the CC2531
Follow the guide in the Zigbee2MQTT documentation.
-
Setup Zigbee2MQTT
The software can be installed in various ways that are all well documented. I chose Docker.
-
Add Map Transformations
reachable.js:
OPEN=ONLINE CLOSED=OFFLINE NULL=?
zigbee2mqtt_log_level.js:
0=DEBUG 1=INFO 2=WARNING 3=ERROR 0.0=DEBUG 1.0=INFO 2.0=WARNING 3.0=ERROR debug=0 info=1 warn=2 error=3
zigbee2mqtt_log_type.js:
pairing=PAIRING device_connected=DEVICE CONNECTED device_ban=DEVICE BAN device_ban_failed=DEVICE BAN FAILED device_announced=DEVICE ANNOUNCED device_removed=DEVICE REMOVE device_removed_failed=DEVICE REMOVE FAILED device_force_removed=DEVICE FORCE REMOVE device_force_removed_failed=DEVICE FORCE REMOVE FAILED device_banned=DEVICE BANNED device_whitelisted=DEVICE WHITELISTED device_renamed=DEVICE RENAMED group_renamed=GROUP RENAMED group_added=GROUP ADDED group_removed=GROUP REMOVED device_bind=DEVICE BOUND device_unbind=DEVICE UNBOUND device_group_add=DEVICE GROUP ADD device_group_add_failed=DEVICE GROUP ADD FAILED device_group_remove=DEVICE GROUP REMOVE device_group_remove_failed=DEVICE GROUP REMOVE FAILED device_group_remove_all=DEVICE GROUP REMOVE ALL device_group_remove_all_failed=DEVICE GROUP REMOVE ALL FAILED devices=LIST ALL DEVICES groups=LIST ALL GROUPS zigbee_publish_error=ZIGBEE PUBLISH ERROR ota_update=OTA UPDATE touchlink=TOUCHLINK
zigbee2mqtt_supported.js:
OPEN=YES CLOSED=NO NULL=UNKNOWN
-
Add Zigbee2MQTT Thing
Create a new Things file and add the following:
Thing mqtt:topic:zigbee2mqtt "Zigbee 2 MQTT" (mqtt:broker:bridge) { Channels: Type contact : state "State" [ stateTopic="zigbee2mqtt/bridge/state", off="offline", on="online"] Type string : version "Version" [ stateTopic="zigbee2mqtt/bridge/config", transformationPattern="JSONPATH:$.version"] Type string : commit "Commit" [ stateTopic="zigbee2mqtt/bridge/config", transformationPattern="JSONPATH:$.commit"] Type string : type "Type" [ stateTopic="zigbee2mqtt/bridge/config", transformationPattern="REGEX:(.*type.*)â©JSONPATH:$.coordinator.type"] Type string : transportrev "Transport Revision" [ stateTopic="zigbee2mqtt/bridge/config", transformationPattern="REGEX:(.*meta.*)â©JSONPATH:$.coordinator.meta.transportrev"] Type string : product "Product" [ stateTopic="zigbee2mqtt/bridge/config", transformationPattern="REGEX:(.*meta.*)â©JSONPATH:$.coordinator.meta.product"] Type string : majorrel "Major Release" [ stateTopic="zigbee2mqtt/bridge/config", transformationPattern="REGEX:(.*meta.*)â©JSONPATH:$.coordinator.meta.majorrel"] Type string : minorrel "Minor Release" [ stateTopic="zigbee2mqtt/bridge/config", transformationPattern="REGEX:(.*meta.*)â©JSONPATH:$.coordinator.meta.minorrel"] Type string : maintrel "Main Release" [ stateTopic="zigbee2mqtt/bridge/config", transformationPattern="REGEX:(.*meta.*)â©JSONPATH:$.coordinator.meta.maintrel"] Type string : revision "Revision" [ stateTopic="zigbee2mqtt/bridge/config", transformationPattern="REGEX:(.*meta.*)â©JSONPATH:$.coordinator.meta.revision"] Type number : log "Log Level" [ stateTopic="zigbee2mqtt/bridge/config", commandTopic="zigbee2mqtt/bridge/config/log_level", transformationPattern="REGEX:(.*log_level.*)â©JSONPATH:$.log_level"] Type switch : permit "Permit Join" [ stateTopic="zigbee2mqtt/bridge/config", commandTopic="zigbee2mqtt/bridge/config/permit_join", transformationPattern="REGEX:(.*permit_join.*)â©JSONPATH:$.permit_join", off="false", on="true"] Type string : logtype "Log type" [ stateTopic="zigbee2mqtt/bridge/log", transformationPattern="JSONPATH:$.type"] Type string : message "Message" [ stateTopic="zigbee2mqtt/bridge/log", transformationPattern="JSONPATH:$.message"] Type string : friendlyname "Friendly name" [ stateTopic="zigbee2mqtt/bridge/log", transformationPattern="REGEX:(.*friendly_name.*)â©JSONPATH:$.meta.friendly_name"] Type string : model "Model" [ stateTopic="zigbee2mqtt/bridge/log", transformationPattern="REGEX:(.*model.*)â©JSONPATH:$.meta.model"] Type string : vendor "Vendor" [ stateTopic="zigbee2mqtt/bridge/log", transformationPattern="REGEX:(.*vendor.*)â©JSONPATH:$.meta.vendor"] Type string : description "Description" [ stateTopic="zigbee2mqtt/bridge/log", transformationPattern="REGEX:(.*description.*)â©JSONPATH:$.meta.description"] Type contact : supported "Supported" [ stateTopic="zigbee2mqtt/bridge/log", transformationPattern="REGEX:(.*supported.*)â©JSONPATH:$.meta.supported", off="false", on="true"] }
-
Add Zigbee2MQTT Item
Put this in an Items file:
Group Zigbee_2_MQTT "Zigbee2MQTT" <zigbee2mqtt> (ZigbeeBridges) Contact Zigbee_2_MQTT_State "State: [MAP(reachable.map):%s]" <contactable> (Zigbee_2_MQTT) { channel="mqtt:topic:zigbee2mqtt:state" } String Zigbee_2_MQTT_Version "Version: [%s]" <zigbee2mqtt> (Zigbee_2_MQTT) { channel="mqtt:topic:zigbee2mqtt:version" } String Zigbee_2_MQTT_Commit "Commit: [%s]" <zigbee2mqtt> (Zigbee_2_MQTT) { channel="mqtt:topic:zigbee2mqtt:commit" } String Zigbee_2_MQTT_Type "Type: [%s]" <zigbee2mqtt> (Zigbee_2_MQTT) { channel="mqtt:topic:zigbee2mqtt:type" } String Zigbee_2_MQTT_Transport "Transport rev: [%s]" <zigbee2mqtt> (Zigbee_2_MQTT) { channel="mqtt:topic:zigbee2mqtt:transportrev" } String Zigbee_2_MQTT_Product "Product: [%s]" <zigbee2mqtt> (Zigbee_2_MQTT) { channel="mqtt:topic:zigbee2mqtt:product" } String Zigbee_2_MQTT_Major_Rel "Major rel: [%s]" <zigbee2mqtt> (Zigbee_2_MQTT) { channel="mqtt:topic:zigbee2mqtt:majorrel" } String Zigbee_2_MQTT_Minor_Rel "Minor rel: [%s]" <zigbee2mqtt> (Zigbee_2_MQTT) { channel="mqtt:topic:zigbee2mqtt:minorrel" } String Zigbee_2_MQTT_Main_Rel "Main rel: [%s]" <zigbee2mqtt> (Zigbee_2_MQTT) { channel="mqtt:topic:zigbee2mqtt:maintrel" } String Zigbee_2_MQTT_Revision "Revision: [%s]" <zigbee2mqtt> (Zigbee_2_MQTT) { channel="mqtt:topic:zigbee2mqtt:revision" } Switch Zigbee_2_MQTT_Permit_Join "Permit Join: []" <zigbee2mqtt> (Zigbee_2_MQTT) { channel="mqtt:topic:zigbee2mqtt:permit" } Group Zigbee_2_MQTT_Logs "Zigbee2MQTT Logs" <text> (Zigbee_2_MQTT) Number Zigbee_2_MQTT_Log_Level "Log level: [MAP(zigbee2mqtt_log_level.map):%s]" <text> (Zigbee_2_MQTT_Logs) { channel="mqtt:topic:zigbee2mqtt:log" } String Zigbee_2_MQTT_Log_Type "Log type: [MAP(zigbee2mqtt_log_type.map):%s]" <text> (Zigbee_2_MQTT_Logs) { channel="mqtt:topic:zigbee2mqtt:logtype" } String Zigbee_2_MQTT_Log_Message "Log message: [%s]" <text> (Zigbee_2_MQTT_Logs) { channel="mqtt:topic:zigbee2mqtt:message" } String Zigbee_2_MQTT_Friendly "Friendly name: [%s]" <text> (Zigbee_2_MQTT_Logs) { channel="mqtt:topic:zigbee2mqtt:friendlyname" } String Zigbee_2_MQTT_Model "Model: [%s]" <text> (Zigbee_2_MQTT_Logs) { channel="mqtt:topic:zigbee2mqtt:model" } String Zigbee_2_MQTT_Vendor "Vendor: [%s]" <text> (Zigbee_2_MQTT_Logs) { channel="mqtt:topic:zigbee2mqtt:vendor" } String Zigbee_2_MQTT_Description "Description: [%s]" <text> (Zigbee_2_MQTT_Logs) { channel="mqtt:topic:zigbee2mqtt:description" } Contact Zigbee_2_MQTT_Supported "Supported: [MAP(zigbee2mqtt_supported.map)%s]" <text> (Zigbee_2_MQTT_Logs) { channel="mqtt:topic:zigbee2mqtt:supported" }
-
Discover Zigbee Things and IDs
I refer you to the Zigbee2MQTT documentation (last time, swear
)
-
Add Zigbee Things
Some of the Sonoff Zigbee things have payload values in common - such as battery_low and tamper between the Motion and Wireless Door/Window sensors.
If you are adding either or both of those devices, create the following Map transformation:
snzb_boolean.js
OPEN=TRUE CLOSED=FALSE NULL=UNKNOWN
1: Wireless Switches
Reports actions. Can be one of the following three values: single, double and long.
-
Add a new Thing to a file
Thing mqtt:topic:snzb01a "My Awesome Button" (mqtt:broker:bridge) @ "Some Fancy Room" { Channels: Type string : action "Action" [ stateTopic="zigbee2mqtt/UNIQUE_ZIGBEE_ID", transformationPattern="REGEX:(.*action.*)â©JSONPATH:$.action"] Type number : linkquality "Link Quality" [ stateTopic="zigbee2mqtt/UNIQUE_ZIGBEE_ID", transformationPattern="JSONPATH:$.linkquality"] Type number : battery "Battery" [ stateTopic="zigbee2mqtt/UNIQUE_ZIGBEE_ID", transformationPattern="JSONPATH:$.battery"] Type number : voltage "Voltage" [ stateTopic="zigbee2mqtt/UNIQUE_ZIGBEE_ID", transformationPattern="JSONPATH:$.voltage"] }
-
Add a new Item to a file
Group ZigbeeButtonActions "Zigbee Buttons" <button> Group SNZB_01_1_Button "My Awesome Button" <sonoff_snzb01> (GF_SomeRoom, Sonoff_SNZB_01) String SNZB_01_1_Button_Action "Action: [%s]" <none> (SNZB_01_1_Button, ZigbeeButtonActions) { channel="mqtt:topic:snzb01a:action" } Number SNZB_01_1_Button_Link "Link quality [%d]" <none> (SNZB_01_1_Button) { channel="mqtt:topic:snzb01a:linkquality" } Number SNZB_01_1_Button_Battery "Battery [%d]" <none> (SNZB_01_1_Button) { channel="mqtt:topic:snzb01a:battery" } Number SNZB_01_1_Button_Voltage "Voltage [%d]" <none> (SNZB_01_1_Button) { channel="mqtt:topic:snzb01a:voltage" }
-
Create a Rule
val String filename = "my_super_cool_zigbee_rule_file.rules" rule "Zigbee button received update" when Member of ZigbeeButtonActions received update then logInfo(filename, "Zigbee button received update: {}", triggeringItem.name) switch (triggeringItem) { case SNZB_01_1_Button_Action: { switch (triggeringItem.state) { case 'single': { // Do stuff } case 'double': { // Do other stuff } case 'long': { // Do super secret stuff } } } } end
2: Temperature and Humidity Sensors
Not covered yet.
-
Add a new Thing to a file
-
Add a new Item to a file
-
Create a Rule
3: Motion Sensors
Reports occupancy.
-
Create Map Transformations
snzb03_occupancy.jsOPEN=OCCUPIED CLOSED=IDLE NULL=UNKNOWN
-
Add a new Thing to a file
Thing mqtt:topic:snzb03a "Important Motion Sensor" (mqtt:broker:bridge) @ "Important Room" { Channels: Type contact : occupancy "Occupancy" [ stateTopic="zigbee2mqtt/UNIQUE_ZIGBEE_ID", transformationPattern="REGEX:(.*occupancy.*)â©JS:snzb03_occupancy.js"] Type contact : tamper "Tamper" [ stateTopic="zigbee2mqtt/UNIQUE_ZIGBEE_ID", transformationPattern="REGEX:(.*tamper.*)â©JS:tamper.js"] Type contact : battery_low "Battery Low" [ stateTopic="zigbee2mqtt/UNIQUE_ZIGBEE_ID", transformationPattern="REGEX:(.*battery_low.*)â©JS:battery_low.js"] Type number : linkquality "Link Quality" [ stateTopic="zigbee2mqtt/UNIQUE_ZIGBEE_ID", transformationPattern="JSONPATH:$.linkquality"] Type number : battery "Battery" [ stateTopic="zigbee2mqtt/UNIQUE_ZIGBEE_ID", transformationPattern="JSONPATH:$.battery"] Type number : voltage "Voltage" [ stateTopic="zigbee2mqtt/UNIQUE_ZIGBEE_ID", transformationPattern="JSONPATH:$.voltage"] }
-
Add a new Item to a file:
Group MotionSensors "Motion Sensors" <motion> Group SNZB_03_1_Motion_Sensor "Important Room Motion Sensor" <sonoff_snzb03> (GF_ImportantRoom, Sonoff_SNZB_03) Contact SNZB_03_1_Motion_Sensor_Occupancy "Important Room [MAP(snzb03_occupancy.map):%s]" <motion> (SNZB_03_1_Motion_Sensor, MotionSensors) { channel="mqtt:topic:snzb03a:occupancy" } Contact SNZB_03_1_Motion_Sensor_Tamper "Tamper [MAP(snzb_boolean.map):%s]" <none> (SNZB_03_1_Motion_Sensor) { channel="mqtt:topic:snzb03a:tamper" } Contact SNZB_03_1_Motion_Sensor_Battery_Low "Battery low [MAP(snzb_boolean.map):%s]" <none> (SNZB_03_1_Motion_Sensor) { channel="mqtt:topic:snzb03a:battery_low" } Number SNZB_03_1_Motion_Sensor_Link "Link quality [%d]" <none> (SNZB_03_1_Motion_Sensor) { channel="mqtt:topic:snzb03a:linkquality" } Number SNZB_03_1_Motion_Sensor_Battery "Battery [%d]" <none> (SNZB_03_1_Motion_Sensor) { channel="mqtt:topic:snzb03a:battery" } Number SNZB_03_1_Motion_Sensor_Voltage "Voltage [%d]" <none> (SNZB_03_1_Motion_Sensor) { channel="mqtt:topic:snzb03a:voltage" }
- Create a new Rule:
val String filename = "best_rule_file_ever.rules" rule "Motion sensor status" when Member of MotionSensors changed then logInfo(filename, triggeringItem.name + ": " + triggeringItem.state) switch (triggeringItem.state) { case OPEN: { switch (triggeringItem) { case SNZB_03_1_Motion_Sensor_Occupancy: { // Motion detected, let the dogs loose! } } } case CLOSED: { switch (triggeringItem) { case SNZB_03_1_Motion_Sensor_Occupancy: { // No more motion detected } } } } end
4: Wireless Door/Window Sensors
Reports contact.
-
Add a new Thing to a file
Thing mqtt:topic:snzb04a "Hidden Door Contact Sensor" (mqtt:broker:bridge) @ "Hidden Room" { Channels: Type contact : sensor "Contact" [ stateTopic="zigbee2mqtt/UNIQUE_ZIGBEE_ID", transformationPattern="REGEX:(.*contact.*)â©JSONPATH:$.contact", on="false", off="true"] Type contact : tamper "Tamper" [ stateTopic="zigbee2mqtt/UNIQUE_ZIGBEE_ID", transformationPattern="REGEX:(.*tamper.*)â©JSONPATH:$.tamper", on="true", off="false"] Type contact : battery_low "Battery Low" [ stateTopic="zigbee2mqtt/UNIQUE_ZIGBEE_ID", transformationPattern="REGEX:(.*battery_low.*)â©JSONPATH:$.battery_low", on="true", off="false"] Type number : linkquality "Link Quality" [ stateTopic="zigbee2mqtt/UNIQUE_ZIGBEE_ID", transformationPattern="JSONPATH:$.linkquality"] Type number : battery "Battery" [ stateTopic="zigbee2mqtt/UNIQUE_ZIGBEE_ID", transformationPattern="JSONPATH:$.battery"] Type number : voltage "Voltage" [ stateTopic="zigbee2mqtt/UNIQUE_ZIGBEE_ID", transformationPattern="JSONPATH:$.voltage"] }
-
Add a new Item to a file
Group DoorSensors "Door Sensors" <door> Group SNZB_04_1_Contact_Sensor "Hidden Door Contact Sensor" <sonoff_snzb04> (GF_HiddenRoom, Sonoff_SNZB_04) Contact SNZB_04_1_Contact_Sensor_Contact "Contact: [%s]" <door> (SNZB_04_1_Contact_Sensor, DoorSensors) { channel="mqtt:topic:snzb04a:sensor" } Contact SNZB_04_1_Contact_Sensor_Tamper "Tamper [MAP(snzb_boolean.map):%s]" <none> (SNZB_04_1_Contact_Sensor) { channel="mqtt:topic:snzb04a:tamper" } Contact SNZB_04_1_Contact_Sensor_Battery_Low "Battery low [MAP(snzb_boolean.map):%s]" <none> (SNZB_04_1_Contact_Sensor) { channel="mqtt:topic:snzb04a:battery_low" } Number SNZB_04_1_Contact_Sensor_Link "Link quality [%d]" <none> (SNZB_04_1_Contact_Sensor) { channel="mqtt:topic:snzb04a:linkquality" } Number SNZB_04_1_Contact_Sensor_Battery "Battery [%d]" <none> (SNZB_04_1_Contact_Sensor) { channel="mqtt:topic:snzb04a:battery" } Number SNZB_04_1_Contact_Sensor_Voltage "Voltage [%d]" <none> (SNZB_04_1_Contact_Sensor) { channel="mqtt:topic:snzb04a:voltage" }
-
Create a Rule
val String filename = "doors_and_windows_and_tings.rules" rule "Door sensor status" when Member of DoorSensors changed then logInfo(filename, triggeringItem.name + ": " + triggeringItem.state) switch (triggeringItem.state) { case OPEN: { switch (triggeringItem) { case SNZB_04_1_Contact_Sensor_Contact: { // The ***ting door is open! } } } case CLOSED: { switch (triggeringItem) { case SNZB_04_1_Contact_Sensor_Contact: { // Closed now } } } } end
-
Done.