diff --git a/.gitignore b/.gitignore deleted file mode 100644 index cc2239a..0000000 --- a/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -.pio -.pioenvs -.piolibdeps -.vscode/.browse.c_cpp.db* -.vscode/c_cpp_properties.json -.vscode/launch.json -data -.vscode/settings.json -.vscode/extensions.json -.DS_Store -scripts \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 1742b2e..c77d9ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@ # Changelog All notable changes to this project will be documented in this file. -## [Unreleased] +## [3.0.0] 2023-01-20 +This is a completely rewritten implementation. Instead of using the Homie convention and the homie-esp8266 library, +this version is implemented using ESPHome. + +BREAKING CHANGES!!! +All the mqtt topics have changed! See README.md for more details. ## [2.0.0] 2019-06-30 Changed to v2.x due to the breaking change in the partition topics diff --git a/README.md b/README.md index d48533e..690d1a4 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ # dscalarm-mqtt -A DSC Alarm bridge to MQTT, intended to run on an ESP8266 device connected to your DSC Alarm system's key bus. It allows you to get notifications of alarm events, arm, disarm and send keypad commands through MQTT over wifi. This enables the integration of your DSC alarm system into a home automation platform such as OpenHAB, Home-assistant, etc. +A DSC Alarm bridge to MQTT, intended to run on an ESP8266 or ESP32 device connected to your +DSC Alarm system's key bus. It allows you to get notifications of alarm events, arm, disarm +and send keypad commands through MQTT over wifi. This enables the integration of your +DSC alarm system into a home automation platform, primarily written for OpenHAB. -It uses the dscKeybusInterface library to communicate with the DSC alarm, and the Homie convention for MQTT communication. +It uses the dscKeybusInterface library to communicate with the DSC alarm, and ESPHome for +the main infrastruture such as OTA, configuration, etc. For a detailed wiring diagram, please see: https://github.com/taligentx/dscKeybusInterface @@ -10,206 +14,134 @@ https://github.com/taligentx/dscKeybusInterface ## Features - MQTT Based -- OTA Updateable (Thanks to Homie) -- Configurable Wifi, MQTT, device id, and Alarm's access code. See the [Setup Instructions](#initial-setup) below -- Tested with OpenHAB, but it should work with other home automation systems. Let me know if you're using it with other home automation systems so I can update this document. +- OTA Updateable (Thanks to ESPHome) +- Updates the DSC Panel's internal clock from NTP +- Tested with OpenHAB, but it should work with other home automation systems. -## Usage Examples -- To arm partition 1 to away mode, publish to `homie/device-id/partition-1/away/set`: `1` -- To disarm it, publish to `homie/device-id/partition-1/away/set`: `0` -- To monitor motions on zone 1 (regardless of armed status), subscribe to `homie/device-id/alarm/openzone-1` -- To know partition 1 alarm has been triggered, subscribe to `homie/device-id/partition-1/alarm` -- When zone 3 triggered an alarm `homie/device-id/alarm/alarmzone-3` will be published with a value of `1` -- To know whether partition 1 is armed, subscribe to `homie/device-id/partition-1/away` for away mode, or `homie/device-id/partition-1/stay` for stay mode +## ESPHome -## Homie -The Homie convention specifies the following syntax for MQTT topics: -`homie/device-id/nodename/xxx` +Please see https://esphome.io for information on how to build / upload this project. -- The `device-id` can be set in Homie configuration - see [Initial Setup](#initial-setup) below. - -## Homie Nodes -- `alarm` for the main alarm functionalities -- `partition-N` for partition related status/commands - -So for `alarm` node, the full MQTT topics will start with `homie/device-id/alarm/`, and for the `partition-N` the full MQTT topics will be `homie/device-id/partition-N/` ## MQTT Topics: - -### General MQTT topics: -- `homie/device-id/alarm/trouble` this corresponds to the "Trouble" light / status of the alarm -- `homie/device-id/alarm/power-trouble` -- `homie/device-id/alarm/battery-trouble` -- `homie/device-id/alarm/fire-alarm-keypad` -- `homie/device-id/alarm/aux-alarm-keypad` -- `homie/device-id/alarm/panic-alarm-keypad` -- `homie/device-id/alarm/panel-time` provides the date/time stored in the alarm system formatted as YYYY-MM-DD HH:mm - -### Partitions: -Each partition is implemented as a homie node, so the base MQTT topic for partition 1 is `homie/device-id/partition-N/` -- `homie/device-id/partition-1/away`: 0 (disarmed) | 1 (armed away) -- `homie/device-id/partition-1/stay`: 0 (disarmed) | 1 (armed stay) -- `homie/device-id/partition-1/alarm`: 0 (no alarm) | 1 (the alarm has been triggered) -- `homie/device-id/partition-1/entry-delay`: 0 | 1 (the system is in entry-delay state) -- `homie/device-id/partition-1/exit-delay`: 0 | 1 (the system is in exit-delay state) -- `homie/device-id/partition-1/fire`: 0 (no alarm) | 1 (fire alarm) -- `homie/device-id/partition-1/access-code`: The access code used to arm/disarm -- `homie/device-id/partition-1/lights`: Status lights in JSON `{ "ready": "ON|OFF", "armed": "ON|OFF", "memory": "ON|OFF", "bypass": "ON|OFF", "trouble": "ON|OFF", "program": "ON|OFF", "fire": "ON|OFF", "backlight": "ON|OFF"}` -- ... -- `homie/device-id/partition-N/xxxx` as above +All topics are prefixed with `device_name/` which by default is `dsc-alarm` but this can be +changed in the yaml file. + +### MQTT messages from the device + +These topics are published by the device. + +| Topic | Payload | +| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `status` | `online` or `offline`. This indicates whether esphome is connected to mqtt. | +| `keybus` | `on` when the keybus is connected. `off` otherwise. | +| `trouble` | `on` when the panel encountered a problem. `off` otherwise. | +| `power-trouble` | `on` when the panel has lost AC power. `off` otherwise. | +| `battery-trouble` | `on` when it has problems with the battery. `off` otherwise. | +| `keypad/alarm` | The device will publish a message to this topic when the corresponding alarm was triggered. Valid values are: `panic`, `aux`, and `fire` | +| `panel-time` | The device will publish the current time maintained by the alarm panel in the format of `YYYY-MM-DD HH:MM` | +| `partition/N/alarm` | `on` when the alarm was triggered. `off` otherwise. | +| `partition/N/armed` | `on` when the alarm is armed. `off` otherwise. | +| `partition/N/armed-stay` | `on` when the alarm is armed in away mode. `off` otherwise. | +| `partition/N/armed-away` | `on` when the alarm is armed in stay mode. `off` otherwise. | +| `partition/N/state` | The current state of partition `N`: `disarmed`, `exit_delay`, `armed_away`, `armed_stay`, or `triggered` | +| `partition/N/lights/ready` | `on` or `off` the current status of the ready LED. | +| `partition/N/lights/armed` | `on` or `off` | +| `partition/N/lights/memory` | `on` or `off` | +| `partition/N/lights/bypass` | `on` or `off` | +| `partition/N/lights/trouble` | `on` or `off` | +| `partition/N/lights/program` | `on` or `off` | +| `partition/N/lights/fire` | `on` or `off` | +| `partition/N/lights/backlight` | `on` or `off` | + +### MQTT commands +Publish an MQTT message to the following topics in order to control the device. + +| Topic | Payload | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `command` | `disarm`: disarm all partitions using the configured access code. | +| `command` | `arm_away`: arm all partitions to away mode | +| `command` | `arm_stay`: arm all partitions to stay mode | +| `command` | `trigger_fire`: trigger the fire alarm | +| `command` | `reboot`: reboot the esp device | +| `command` | `reset-status`: resets the internal status so all values are re-published. | +| `write` | Sends the given payload directly to the DSC keybus. This can be used to program the alarm, for example. | +| `panel-time/update` | `now`: instructs the device to update the DSC alarm's internal clock based on the ESP's NTP clock. Normally this is done automatically once a month. | +| `partition/N/armed/set` | `on` to arm partition `N`. `off` to disarm it. | +| `zone/N/motion` | `on` when motion is detected. `off` otherwise. | +| `zone/N/alarm` | `on` when an alarm is triggered by this zone. `off` otherwise. | ### To arm/disarm a partition, publish to: -- `homie/device-id/partition-N/away/set`: 1 | on | arm = arm partition N - away mode. 0 | off | disarm = disarm -- `homie/device-id/partition-N/stay/set`: 1 | on | arm = arm partition N - stay mode. 0 | off | disarm = disarm - -### Zones: -#### General Motion Detection -These will be published whenever motion is detected within the zone, regardless of the armed state: -- `homie/device-id/alarm/openzone-1`: 0 (no movement) | 1 (movement detected) -- ... -- `homie/device-id/alarm/openzone-N`: as above - -#### Triggered Alarm Zones -These will be published when the alarm was triggered by zone motion detection during the armed state: -- `homie/device-id/alarm/alarmzone-1`: 0|1 -- ... -- `homie/device-id/alarm/alarmzone-N`: as above - -## Commands -### To request status refresh, publish to -- `homie/device-id/alarm/refresh-status/set`: 1 - -### To set the panel date/time, publish to -- `homie/device-id/alarm/panel-time/set`: "YYYY-MM-DD HH:mm" - -### Miscellaneous -- `homie/device-id/alarm/maintenance/set`: "reboot" to reboot -- `homie/device-id/alarm/maintenance/set`: "dsc-stop" to stop dsc keybus interrupts -- `homie/device-id/alarm/maintenance/set`: "dsc-start" to start dsc keybus interface - -The DSC Keybus interrupts seem to interfere with the OTA and Config update operation (when publishing to `homie/device-id/$implementation/config/set`), causing the ESP8266 to reset, and in the case of configuration update, to cause a corruption in the Homie configuration file and put Homie into the initial configuration mode. - -So before sending a config update, stop the DSC interrupts by publishing to `homie/device-id/alarm/maintenance/set`: "dsc-stop". Once the config update has been made, restart the DSC interface using `homie/device-id/alarm/maintenance/set`: "dsc-start" or sending a reboot request. - -## Initial Setup -Homie needs to be configured before it can connect to your Wifi / MQTT server. -Standard Homie configuration methods are of course supported. However, the easiest is to follow these steps: - -### Manual / CLI way -- Create a file called config.json, replacing the values accordingly -``` -{ - "wifi": { - "ssid":"Your-Wifi-SSID", - "password":"WifiKey" - }, - "mqtt":{ - "host":"YOUR-MQTT-SERVER", - "port":1883, - "auth":false, - "username":"", - "password":"" - }, - "name":"DSC Alarm", - "ota":{"enabled":true}, - "device_id":"dsc-alarm", - "settings":{"access-code":"1234"} -} -``` -- Connect to **"Homie-xxxxx"** access point -- Type: -``` -curl -X PUT http://192.168.123.1/config --header "Content-Type: application/json" -d @config.json -``` - -### GUI Setup - -You can upload the GUI setup into your ESP8266 SPIFF. See https://github.com/homieiot/homie-esp8266/tree/develop/data/homie - -## Updating The Stored Alarm Access Code - -Before updating Homie config, the DSC interface needs to be deactivated / stopped, because it interferes with writing the configuration file. To do this, publish an MQTT message to -`homie/device-id/alarm/maintenance/set` `dsc-stop` - -After setting the configuration, reactivate the DSC interface by publishing to `homie/device-id/alarm/maintenance/set` `dsc-start` - -To change/update your access code that's stored on the device once it's operational (i.e. connected to your MQTT server), publish to -`homie/device-id/$implementation/config/set {"settings":{"access-code":"1234"}}` -e.g. -``` -mosquitto_pub -t 'homie/device-id/alarm/maintenance/set' -m dsc-stop -mosquitto_pub -t 'homie/device-id/$implementation/config/set' -m '{"settings":{"access-code":"1234"}}' -mosquitto_pub -t 'homie/device-id/alarm/maintenance/set' -m dsc-start -``` - -Note -- The last step may be unnecessary because the ESP8266 seems to crash and restart, but the config gets updated. -- You can update the wifi / mqtt connection details in the same manner +- `dsc-alarm/partition/N/armed/set`: `on` to arm in away mode, `off` to disarm ## Library Dependencies -- [Homie-esp8266 v3.x](https://github.com/homieiot/homie-esp8266.git#develop-v3) -- [dscKeybusReader v1.3](https://github.com/taligentx/dscKeybusInterface.git#develop) +- [dscKeybusReader v3.0](https://github.com/taligentx/dscKeybusInterface.git) ## Tips When Working with the Alarm -- My alarm would go off when I opened my panel enclosure. It uses zone 5 for this detection. In order to stop it from going off, bypass zone 5 using `*1` to enter bypass mode. The zone LEDs will light up for bypassed zones. To toggle the zone bypass, enter the 2 digit zone number, e.g. `05`. When zone 5 is lit up, it will be bypassed. Press `#` to return to ready state. - -- While working/testing the alarm, disconnect the internal / external speakers, and replace it with a 10K resistor. This will avoid disturbing the neighbours. +- My alarm would go off when I opened my panel enclosure. It uses zone 5 for this detection. + In order to stop it from going off, bypass zone 5 using `*1` to enter bypass mode. + The zone LEDs will light up for bypassed zones. To toggle the zone bypass, enter the + 2 digit zone number, e.g. `05`. When zone 5 is lit up, it will be bypassed. + Press `#` to return to ready state. -## OpenHAB Example +- While working/testing the alarm, disconnect the internal / external speakers, and replace it + with a 10K resistor. This will avoid disturbing the neighbours. -The Homie implementation in OpenHAB isn't working for me, so I created manual things/items files instead. +## OpenHAB Integration ### MQTT Broker thing I have a separate mqtt.things to define the broker bridge. It can be used/referenced by mqtt things in other files. -``` + +```java // Adjust the connection settings accordingly Bridge mqtt:broker:mosquitto [ host="x.x.x.x", secure="false" ] ``` ### dscalarm.things + This assumes that you've defined an MQTT bridge thing called `mqtt:broker:mosquito` (as above). -``` -Thing mqtt:topic:mosquitto:dsc "Alarm System" (mqtt:broker:mosquitto) @ "Alarm" { + +```java +Thing mqtt:topic:dsc "Alarm System" (mqtt:broker:mosquitto) { Channels: - Type contact : trouble "Trouble" [ stateTopic="homie/dsc-alarm/alarm/trouble", on="1", off="0" ] - Type contact : power_trouble "Power Trouble" [ stateTopic="homie/dsc-alarm/alarm/power-trouble", on="1", off="0" ] - Type contact : battery_trouble "Battery Trouble" [ stateTopic="homie/dsc-alarm/alarm/battery-trouble", on="1", off="0" ] - Type contact : fire_alarm_keypad "Fire Alarm Keypad" [ stateTopic="homie/dsc-alarm/alarm/fire-alarm-keypad", on="1", off="0" ] - Type contact : aux_alarm_keypad "Aux Alarm Keypad" [ stateTopic="homie/dsc-alarm/alarm/aux-alarm-keypad", on="1", off="0" ] - Type contact : panic_alarm_keypad "Panic Alarm Keypad" [ stateTopic="homie/dsc-alarm/alarm/panic-alarm-keypad", on="1", off="0" ] - - Type switch : partition_1_away "Away Mode" [ stateTopic="homie/dsc-alarm/partition-1/away", commandTopic="homie/dsc-alarm/partition-1/away/set", on="1", off="0" ] - Type switch : partition_1_stay "Stay Mode" [ stateTopic="homie/dsc-alarm/partition-1/stay", commandTopic="homie/dsc-alarm/partition-1/stay/set", on="1", off="0" ] - Type contact : partition_1_alarm "Alarm" [ stateTopic="homie/dsc-alarm/partition-1/alarm", on="1", off="0" ] - Type contact : partition_1_fire "Fire Alarm" [ stateTopic="homie/dsc-alarm/partition-1/fire", on="1", off="0" ] - - Type contact : openzone_1 "Living Room" [ stateTopic="homie/dsc-alarm/alarm/openzone-1", on="1", off="0" ] - Type contact : openzone_2 "Lounge Room" [ stateTopic="homie/dsc-alarm/alarm/openzone-2", on="1", off="0" ] - Type contact : openzone_3 "Bedroom 1" [ stateTopic="homie/dsc-alarm/alarm/openzone-3", on="1", off="0" ] - Type contact : openzone_4 "Bedroom 2" [ stateTopic="homie/dsc-alarm/alarm/openzone-4", on="1", off="0" ] - Type contact : openzone_5 "Panel Open" [ stateTopic="homie/dsc-alarm/alarm/openzone-5", on="1", off="0" ] - Type contact : openzone_6 "Siren Tampered" [ stateTopic="homie/dsc-alarm/alarm/openzone-6", on="1", off="0" ] - - Type contact : alarmzone_1 "Living Room Triggered" [ stateTopic="homie/dsc-alarm/alarm/alarmzone-1", on="1", off="0" ] - Type contact : alarmzone_2 "Lounge Room Triggered" [ stateTopic="homie/dsc-alarm/alarm/alarmzone-2", on="1", off="0" ] - Type contact : alarmzone_3 "Bedroom 1 Triggered" [ stateTopic="homie/dsc-alarm/alarm/alarmzone-3", on="1", off="0" ] - Type contact : alarmzone_4 "Bedroom 2 Triggered" [ stateTopic="homie/dsc-alarm/alarm/alarmzone-4", on="1", off="0" ] - Type contact : alarmzone_5 "Panel Open Triggered" [ stateTopic="homie/dsc-alarm/alarm/alarmzone-5", on="1", off="0" ] - Type contact : alarmzone_6 "Siren Tampered Triggered" [ stateTopic="homie/dsc-alarm/alarm/alarmzone-6", on="1", off="0" ] -} + Type contact : trouble "Trouble" [ stateTopic="dsc-alarm/trouble", on="on", off="off" ] + Type contact : power_trouble "Power Trouble" [ stateTopic="dsc-alarm/power-trouble", on="on", off="off" ] + Type contact : battery_trouble "Battery Trouble" [ stateTopic="dsc-alarm/battery-trouble", on="on", off="off" ] + Type string : keypad_alarm "Keypad Alarm" [ stateTopic="dsc-alarm/keypad/alarm" ] + + Type switch : partition_1_armed "Alarm Armed" [ stateTopic="dsc-alarm/partition/1/armed", commandTopic="dsc-alarm/partition/1/armed/set" ] + + Type contact : partition_1_alarm "Alarm" [ stateTopic="dsc-alarm/partition/1/alarm", on="on", off="off" ] + Type string : partition_1_state "State" [ stateTopic="dsc-alarm/partition/1/state" ] + + Type contact : motionzone_1 "Living Room" [ stateTopic="dsc-alarm/zone/1/motion", on="on", off="off" ] + Type contact : motionzone_2 "Lounge Room" [ stateTopic="dsc-alarm/zone/2/motion", on="on", off="off" ] + Type contact : motionzone_3 "Master Bedroom" [ stateTopic="dsc-alarm/zone/3/motion", on="on", off="off" ] + Type contact : motionzone_4 "Study Room" [ stateTopic="dsc-alarm/zone/4/motion", on="on", off="off" ] + Type contact : motionzone_5 "Panel Open" [ stateTopic="dsc-alarm/zone/5/motion", on="on", off="off" ] + Type contact : motionzone_6 "Siren Tampered" [ stateTopic="dsc-alarm/zone/6/motion", on="on", off="off" ] + + Type contact : alarmzone_1 "Living Room Triggered" [ stateTopic="dsc-alarm/zone/1/alarm", on="on", off="off" ] + Type contact : alarmzone_2 "Lounge Room Triggered" [ stateTopic="dsc-alarm/zone/2/alarm", on="on", off="off" ] + Type contact : alarmzone_3 "Master Bedroom Triggered" [ stateTopic="dsc-alarm/zone/3/alarm", on="on", off="off" ] + Type contact : alarmzone_4 "Study Triggered" [ stateTopic="dsc-alarm/zone/4/alarm", on="on", off="off" ] + Type contact : alarmzone_5 "Panel Open Triggered" [ stateTopic="dsc-alarm/zone/5/alarm", on="on", off="off" ] + Type contact : alarmzone_6 "Siren Tampered Triggered" [ stateTopic="dsc-alarm/zone/6/alarm", on="on", off="off" ] +} } ``` ### dscalarm.items -Here the `Switchable` tag is to expose the alarm to Google Home. -``` -Contact Alarm_Trouble "Trouble [MAP(dsc-alarm-indicator.map):%s]" { channel="mqtt:topic:mosquitto:dsc:trouble" } -Switch Alarm_Armed "Alarm" ["Switchable"] { autoupdate="false", channel="mqtt:topic:mosquitto:dsc:partition_1_away" } -Contact Alarm_Triggered "Alarm Triggered" { channel="mqtt:topic:mosquitto:dsc:partition_1_alarm" } -Contact Alarm_Living_Room_Sensor "Living Room Sensor" { channel="mqtt:topic:mosquitto:dsc:openzone_1" } -Contact Alarm_Lounge_Room_Sensor "Lounge Room Sensor" { channel="mqtt:topic:mosquitto:dsc:openzone_2" } -Contact Alarm_Bedroom1_Sensor "Bedroom 1 Sensor" { channel="mqtt:topic:mosquitto:dsc:openzone_3" } -Contact Alarm_Bedroom2_Sensor "Bedroom 2 Sensor" { channel="mqtt:topic:mosquitto:dsc:openzone_4" } +```java +Switch Alarm_Armed "Alarm Armed" {channel="mqtt:topic:dsc:partition_1_armed", autoupdate="false"} +Contact Alarm_Trouble "Trouble" {channel="mqtt:topic:dsc:trouble"} +String Alarm_State "Alarm State" {channel="mqtt:topic:dsc:partition_1_state"} +Contact Alarm_Triggered "Alarm Triggered" {channel="mqtt:topic:dsc:partition_1_alarm"} + +Contact LivingRoom_Motion "Living Room Motion" {channel="mqtt:topic:dsc:motionzone_1"} +Contact LoungeRoom_Motion "Lounge Room Motion" {channel="mqtt:topic:dsc:motionzone_2"} +Contact MasterBedRoom_Motion "Master Bed Room Motion" {channel="mqtt:topic:dsc:motionzone_3"} +Contact StudyRoom_Motion "Study Room Motion" {channel="mqtt:topic:dsc:motionzone_4"} ``` ### dcalarm.rules @@ -226,10 +158,7 @@ then end ``` -### dsc-alarm-indicator.map -``` -1=Yes -OPEN=Yes -0=No -CLOSED=No -``` \ No newline at end of file +## Credits + +* Thanks to [@taligentx](https://github.com/taligentx) for the wonderful [dscKeybusinterface library](https://github.com/taligentx/dscKeybusInterface). +* The implementation of the custom component was inspired by https://github.com/Dilbert66/esphome-dsckeybus diff --git a/common.yaml b/common.yaml new file mode 100644 index 0000000..9d438d5 --- /dev/null +++ b/common.yaml @@ -0,0 +1,59 @@ +esphome: + name: ${device_name} + comment: ${friendly_name} + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + reboot_timeout: 1h + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + # ssid: "ESPHOME" + ssid: ${device_name} + password: !secret fallback_hotspot_password + +captive_portal: + +ota: + password: !secret ota_password + +logger: + level: INFO + +web_server: + port: 80 + auth: + username: !secret web_login + password: !secret web_password + +switch: + - platform: restart + name: reboot + retain: false + +sensor: + - platform: wifi_signal + id: wifi_signal_db + name: rssi + update_interval: 300s + - platform: copy # Reports the WiFi signal strength in % + source_id: wifi_signal_db + name: signal + filters: + - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0); + unit_of_measurement: "Signal %" + - platform: uptime + name: uptime + +text_sensor: + - platform: version + name: esphome_version + - platform: wifi_info + ip_address: + name: ip-address + +mqtt: + broker: !secret mqtt_broker + discovery: false + reboot_timeout: 1h + id: mqtt_id diff --git a/dsc-alarm.yaml b/dsc-alarm.yaml new file mode 100644 index 0000000..c38aa59 --- /dev/null +++ b/dsc-alarm.yaml @@ -0,0 +1,250 @@ +substitutions: + # Adjust accordingly + device_name: dsc-alarm + friendly_name: Alarm System + + timezone: Australia/Brisbane + # set this to your local NTP server if you have one + ntp_server: 2.pool.ntp.org + + access_code: !secret alarm_access_code + + dsc_clock_pin: D1 + dsc_read_pin: D2 + dsc_write_pin: D8 + + max_zone: "6" + max_partition: "1" + +esp8266: + # change this to match your board + board: d1_mini + +packages: + common: !include common.yaml + +esphome: + includes: + - includes/dscAlarm.h + libraries: + - taligentx/dscKeybusInterface@^3.0 + +ota: + on_begin: + - lambda: ((DSCKeybus*)id(dsc_keybus))->disconnect(); + on_error: + - lambda: ((DSCKeybus*)id(dsc_keybus))->connect(); + +time: + - platform: sntp + id: ntp_time + timezone: ${timezone} + servers: ["${ntp_server}", 0.pool.ntp.org, 1.pool.ntp.org] + on_time_sync: + then: + - lambda: |- + ESP_LOGI("main", "System clock synchronized %s", id(ntp_time).now().strftime("%F %R").c_str()); + + on_time: + - seconds: 0 + minutes: 0 + hours: 9 + days_of_month: 1 + then: + - lambda: |- + auto time = id(ntp_time).now(); + ESP_LOGI("main", "Updating Panel time to %s", time.strftime("%F %R").c_str()); + ((DSCKeybus*)id(dsc_keybus))->setTime(time.year, time.month, time.day_of_month, time.hour, time.minute); + +mqtt: + on_connect: + - delay: 1s + - lambda: ((DSCKeybus*)id(dsc_keybus))->resetStatus(); + + on_message: + - topic: ${device_name}/command + payload: disarm + then: + - lambda: |- + for (auto i = 1; i <= ${max_partition}; i++) { + ((DSCKeybus*)id(dsc_keybus))->disarm(i); + } + + - topic: ${device_name}/command + payload: arm_away + then: + - lambda: |- + for (auto i = 1; i <= ${max_partition}; i++) { + ((DSCKeybus*)id(dsc_keybus))->armAway(i); + } + + - topic: ${device_name}/command + payload: arm_stay + then: + - lambda: |- + for (auto i = 1; i <= ${max_partition}; i++) { + ((DSCKeybus*)id(dsc_keybus))->armStay(i); + } + + - topic: ${device_name}/command + payload: trigger_fire + then: + - lambda: ((DSCKeybus*)id(dsc_keybus))->triggerFireAlarm(); + + - topic: ${device_name}/command + payload: trigger_panic + then: + - lambda: ((DSCKeybus*)id(dsc_keybus))->triggerPanicAlarm(); + + - topic: ${device_name}/command + payload: reboot + then: + - lambda: App.safe_reboot(); + + - topic: ${device_name}/command + payload: reset-status + then: + - lambda: ((DSCKeybus*)id(dsc_keybus))->resetStatus(); + + - topic: ${device_name}/write + then: + - lambda: ((DSCKeybus*)id(dsc_keybus))->write(x); + + # Updates partition 1's time from NTP + - topic: ${device_name}/panel-time/update + payload: now + then: + - lambda: |- + auto time = id(ntp_time).now(); + if (time.is_valid()) { + ((DSCKeybus*)id(dsc_keybus))->setTime(time.year, time.month, time.day_of_month, time.hour, time.minute); + } + +custom_component: + - lambda: |- + auto dscKeybus = new DSCKeybus(${dsc_clock_pin}, ${dsc_read_pin}, ${dsc_write_pin}, "${access_code}"); + + auto on_or_off = [](bool value) { return (std::string)(value ? "on" : "off"); }; + auto extract_number = [](const char *prefix, const std::string &value) { + auto start = strlen(prefix); + auto end = value.find('/', start); + return parse_number(value.substr(start, end-start)).value_or(0); + }; + + dscKeybus->onKeybusConnectionChange([&](bool isConnected) { + id(mqtt_id)->publish("${device_name}/keybus", on_or_off(isConnected), 0, true); + }); + + dscKeybus->onKeypadStatusChange([&](DSCKeybus::KeypadStatus statusType, bool state) { + std::string topic = "${device_name}/"; + switch(statusType) { + case DSCKeybus::KeypadStatus::TROUBLE: topic += "trouble"; break; + case DSCKeybus::KeypadStatus::BATTERY_TROUBLE: topic += "battery-trouble"; break; + case DSCKeybus::KeypadStatus::POWER_TROUBLE: topic += "power-trouble"; break; + } + id(mqtt_id)->publish(topic, on_or_off(state), 0, true); + }); + + dscKeybus->onKeypadAlarm([&](DSCKeybus::KeypadAlarm alarmType) { + std::string type; + switch (alarmType) { + case DSCKeybus::KeypadAlarm::PANIC_ALARM: type = "panic"; break; + case DSCKeybus::KeypadAlarm::AUX_ALARM: type = "aux"; break; + case DSCKeybus::KeypadAlarm::FIRE_ALARM: type = "fire"; break; + } + id(mqtt_id)->publish("${device_name}/keypad/alarm", type); + }); + + dscKeybus->onPanelTimeChange([&](std::string time) { + id(mqtt_id)->publish("${device_name}/panel-time", time); + }); + + dscKeybus->onLightStatusChange([&](uint8_t partition, byte lights) { + if (partition > ${max_partition}) { + return; + } + + static byte previousLights[${max_partition}]; + + std::string topic = str_sprintf("${device_name}/partition/%d/lights/", partition); + const char* lightNames[] = { "ready", "armed", "memory", "bypass", "trouble", "program", "fire", "backlight" }; + + for (byte i = 0; i < 8; i++) { + bool light = bitRead(lights, i); + if (light != bitRead(previousLights[partition-1], i)) { + id(mqtt_id)->publish(topic + lightNames[i], on_or_off(light)); + } + } + + previousLights[partition - 1] = lights; + }); + + dscKeybus->onPartitionArmedChange([&](uint8_t partition, bool armed, bool armedStay, bool armedAway) { + if (partition > ${max_partition}) { + return; + } + + std::string topic = str_sprintf("${device_name}/partition/%d/", partition); + id(mqtt_id)->publish(topic + "armed", on_or_off(armed), 0, true); + id(mqtt_id)->publish(topic + "armed-stay", on_or_off(armedStay), 0, true); + id(mqtt_id)->publish(topic + "armed-away", on_or_off(armedAway), 0, true); + }); + + id(mqtt_id)->subscribe("${device_name}/partition/+/armed/set", [=](const std::string &topic, const std::string &payload) { + auto partition = extract_number("${device_name}/partition/", topic); + if (partition > dscPartitions) { + ESP_LOGE("main", "Partition out of range: %d", partition); + return; + } + + switch(parse_on_off(payload.c_str())) { + case PARSE_ON: dscKeybus->armAway(partition); break; + case PARSE_OFF: dscKeybus->disarm(partition); break; + } + }); + + dscKeybus->onPartitionAlarmChange([&](uint8_t partition, bool triggered) { + if (partition > ${max_partition}) { + return; + } + + std::string topic = str_sprintf("${device_name}/partition/%d/alarm", partition); + id(mqtt_id)->publish(topic, on_or_off(triggered), 0, true); + }); + + dscKeybus->onPartitionStatusChange([&](uint8_t partition, DSCKeybus::PartitionStatus statusCode) { + if (partition > ${max_partition}) { + return; + } + std::string status; + switch(statusCode) { + case DSCKeybus::PartitionStatus::DISARMED: status = "disarmed"; break; + case DSCKeybus::PartitionStatus::ARMED_STAY: status = "armed_stay"; break; + case DSCKeybus::PartitionStatus::ARMED_AWAY: status = "armed_away"; break; + case DSCKeybus::PartitionStatus::EXIT_DELAY: status = "exit_delay"; break; + case DSCKeybus::PartitionStatus::TRIGGERED: status = "triggered"; break; + } + std::string topic = str_sprintf("${device_name}/partition/%d/state", partition); + id(mqtt_id)->publish(topic, status, 0, true); + }); + + dscKeybus->onZoneStatusChange([&](uint8_t zone, bool state) { + if (zone > ${max_zone}) { + return; + } + std::string topic = str_sprintf("${device_name}/zone/%d/motion", zone); + id(mqtt_id)->publish(topic, on_or_off(state)); + }); + + dscKeybus->onZoneAlarmChange([&](uint8_t zone, bool state) { + if (zone > ${max_zone}) { + return; + } + std::string topic = str_sprintf("${device_name}/zone/%d/alarm", zone); + id(mqtt_id)->publish(topic, on_or_off(state)); + }); + + return {dscKeybus}; + + components: + - id: dsc_keybus diff --git a/includes/dscAlarm.h b/includes/dscAlarm.h new file mode 100644 index 0000000..1aa195e --- /dev/null +++ b/includes/dscAlarm.h @@ -0,0 +1,534 @@ +/** + * + * ESPHome Component to integrate DSC Keybus Interface + * Home: https://github.com/jimtng/esphome-dscalarm + * + * Based on https://github.com/Dilbert66/esphome-dsckeybus, but heavily modified, so it is not + * compatible with it. + * + */ + +#include "esphome.h" +#include "dscKeybusInterface.h" + +class DSCKeybus : public Component { + public: + enum KeypadStatus { TROUBLE, POWER_TROUBLE, BATTERY_TROUBLE }; + enum KeypadAlarm { FIRE_ALARM, AUX_ALARM, PANIC_ALARM }; + enum PartitionStatus { DISARMED, ARMED_STAY, ARMED_AWAY, EXIT_DELAY, TRIGGERED }; + + enum AlarmCommand { + ARM_STAY = 's', + ARM_AWAY = 'w', + TRIGGER_PANIC_ALARM = 'p', + TRIGGER_FIRE_ALARM = 'f', + DISARM = 'd' + }; + + DSCKeybus(byte clockPin, byte readPin, byte writePin, const char *accessCode) + : dsc(clockPin, readPin, writePin), accessCode(accessCode) {} + + void onKeybusConnectionChange(std::function callback) { + keybusConnectionChangeCallback = callback; + } + + void onKeypadStatusChange(std::function callback) { + keypadStatusChangeCallback = callback; + } + + void onKeypadAlarm(std::function callback) { keypadAlarmCallback = callback; } + + void onPanelTimeChange(std::function callback) { panelTimeChangeCallback = callback; } + + void onPartitionAlarmChange(std::function callback) { + partitionAlarmChangeCallback = callback; + } + + void onPartitionArmedChange( + std::function callback) { + partitionArmedChangeCallback = callback; + } + + void onPartitionStatusMessage(std::function callback) { + partitionStatusMessageCallback = callback; + } + + void onPartitionStatusChange(std::function callback) { + partitionStatusChangeCallback = callback; + } + + // lights is a bitmap for: + // const char* lightNames[] = { "ready", "armed", "memory", "bypass", "trouble", "program", "fire", "backlight" }; + void onLightStatusChange(std::function callback) { + lightStatusChangeCallback = callback; + } + + void onZoneStatusChange(std::function callback) { + zoneStatusChangeCallback = callback; + } + + void onZoneAlarmChange(std::function callback) { + zoneAlarmChangeCallback = callback; + } + + void onFireStatusChange(std::function callback) { + fireStatusChangeCallback = callback; + } + + bool isConnected() { return dsc.keybusConnected; } + + void resetStatus() { + ESP_LOGI(LOG_PREFIX, "Resetting status"); + dsc.resetStatus(); + dsc.timestampChanged = true; + for (auto i = 0; i < dscPartitions; i++) { + previousLights[i] = 0xFF; + previousStatus[i] = 0xFF; + } + } + + void disconnect() { + ESP_LOGI(LOG_PREFIX, "Disconnecting from keybus"); + dsc.stop(); + dsc.keybusChanged = true; + dsc.keybusConnected = false; + dsc.statusChanged = false; + } + + void connect() { + ESP_LOGI(LOG_PREFIX, "Connecting to keybus"); + dsc.processModuleData = false; // Controls if keypad and module data is processed and displayed (default: false) + dsc.resetStatus(); + dsc.begin(); + } + + void disarm(uint8_t partition = 1, const char *code = nullptr) { + sendAlarmCommand(partition, AlarmCommand::DISARM, code); + } + void armStay(uint8_t partition = 1) { sendAlarmCommand(partition, AlarmCommand::ARM_STAY); } + void armAway(uint8_t partition = 1) { sendAlarmCommand(partition, AlarmCommand::ARM_AWAY); } + void triggerFireAlarm(uint8_t partition = 1) { sendAlarmCommand(partition, AlarmCommand::TRIGGER_FIRE_ALARM); } + void triggerPanicAlarm(uint8_t partition = 1) { sendAlarmCommand(partition, AlarmCommand::TRIGGER_PANIC_ALARM); } + void write(const char key, uint8_t partition = 1) { + dsc.writePartition = partition; + dsc.write(key); + } + bool write(std::string keystring, uint8_t partition = 1) { + static char buffer[50]; + if (!dsc.writeReady) { + return false; + } + + auto end = keystring.copy(buffer, sizeof(buffer) - 1); + buffer[end] = '\0'; + ESP_LOGD(LOG_PREFIX, "Writing keys: %s", buffer); + dsc.writePartition = partition; + dsc.write(buffer); + return true; + } + bool writeReady() { return dsc.writeReady; } + bool setTime(unsigned int year, byte month, byte day, byte hour, byte minute, byte timePartition = 1) { + return dsc.setTime(year, month, day, hour, minute, accessCode, timePartition); + } + + private: + std::function keybusConnectionChangeCallback; + std::function keypadStatusChangeCallback; + std::function keypadAlarmCallback; + + std::function partitionStatusChangeCallback; + std::function partitionStatusMessageCallback; + std::function partitionAlarmChangeCallback; + std::function partitionArmedChangeCallback; + std::function fireStatusChangeCallback; + + std::function zoneStatusChangeCallback; + std::function zoneAlarmChangeCallback; + + std::function panelTimeChangeCallback; + std::function lightStatusChangeCallback; + + const char *LOG_PREFIX = "dsc"; + dscKeybusInterface dsc; + const char *accessCode; + byte previousStatus[dscPartitions]; + byte previousLights[dscPartitions]; + + void notifyKeybusConnectionChange(bool isConnected) { + if (keybusConnectionChangeCallback) { + keybusConnectionChangeCallback(isConnected); + } + } + + void notifyKeypadStatusChange(KeypadStatus status, bool isActive) { + if (keypadStatusChangeCallback) { + keypadStatusChangeCallback(status, isActive); + } + } + + void notifyKeypadAlarm(KeypadAlarm alarmType) { + if (keypadAlarmCallback) { + keypadAlarmCallback(alarmType); + } + } + + void notifyPartitionStatusMessage(uint8_t partition, byte status) { + if (!partitionStatusMessageCallback) { + return; + } + std::string msg = str_sprintf("%02X: %s", status, String(statusText(status)).c_str()); + partitionStatusMessageCallback(partition + 1, msg); + } + + void notifyPartitionStatusChange(uint8_t partition, PartitionStatus status) { + if (partitionStatusChangeCallback) { + partitionStatusChangeCallback(partition + 1, status); + } + } + + void setup() override { + dsc.resetStatus(); + dsc.begin(); + } + + void sendAlarmCommand(uint8_t partition, AlarmCommand command, const char *code = nullptr) { + if (partition > dscPartitions) { + ESP_LOGE(LOG_PREFIX, "Partition number out of range: %d", partition); + return; + } + auto partition_index = partition - 1; + switch (command) { + case AlarmCommand::DISARM: + if (dsc.armed[partition_index] || dsc.exitDelay[partition_index]) { + dsc.writePartition = partition; + if (!code) { + code = this->accessCode; + } + dsc.write(code); + } + break; + + case AlarmCommand::ARM_STAY: + case AlarmCommand::ARM_AWAY: + if (dsc.armed[partition_index] || dsc.exitDelay[partition_index]) { + break; + } + // fall through below + case AlarmCommand::TRIGGER_FIRE_ALARM: + case AlarmCommand::TRIGGER_PANIC_ALARM: + dsc.writePartition = partition; + dsc.write(command); + break; + } + } + + void loop() override { + static uint8_t zone; + + dsc.loop(); + if (!dsc.statusChanged) { + return; + } + dsc.statusChanged = false; + + if (dsc.bufferOverflow) { + ESP_LOGE(LOG_PREFIX, "Keybus buffer overflow"); + dsc.bufferOverflow = false; + } + + if (dsc.keybusChanged) { + dsc.keybusChanged = false; + notifyKeybusConnectionChange(dsc.keybusConnected); + } + + if (dsc.accessCodePrompt && dsc.writeReady) { + dsc.accessCodePrompt = false; + ESP_LOGD(LOG_PREFIX, "got access code prompt"); + dsc.write(accessCode); + } + + if (dsc.troubleChanged) { + dsc.troubleChanged = false; + notifyKeypadStatusChange(KeypadStatus::TROUBLE, dsc.trouble); + } + + if (dsc.powerChanged) { + dsc.powerChanged = false; + notifyKeypadStatusChange(KeypadStatus::POWER_TROUBLE, dsc.powerTrouble); + } + + if (dsc.batteryChanged) { + dsc.batteryChanged = false; + notifyKeypadStatusChange(KeypadStatus::BATTERY_TROUBLE, dsc.batteryTrouble); + } + + if (dsc.keypadFireAlarm) { + dsc.keypadFireAlarm = false; + notifyKeypadAlarm(KeypadAlarm::FIRE_ALARM); + } + + if (dsc.keypadAuxAlarm) { + dsc.keypadAuxAlarm = false; + notifyKeypadAlarm(KeypadAlarm::AUX_ALARM); + } + + if (dsc.keypadPanicAlarm) { + dsc.keypadPanicAlarm = false; + notifyKeypadAlarm(KeypadAlarm::PANIC_ALARM); + } + + for (byte partition = 0; partition < dscPartitions; partition++) { + if (dsc.disabled[partition]) { + continue; + } + + if (previousStatus[partition] != dsc.status[partition]) { + previousStatus[partition] = dsc.status[partition]; + notifyPartitionStatusMessage(partition, dsc.status[partition]); + } + + if (dsc.alarmChanged[partition] && partitionAlarmChangeCallback) { + dsc.alarmChanged[partition] = false; + partitionAlarmChangeCallback(partition + 1, dsc.alarm[partition]); + } + + bool updateDisarmed = false; + + if (dsc.armedChanged[partition]) { + dsc.armedChanged[partition] = false; + if (partitionArmedChangeCallback) { + partitionArmedChangeCallback(partition + 1, dsc.armed[partition], dsc.armedStay[partition], + dsc.armedAway[partition]); + } + + if (dsc.armed[partition]) { + auto status = dsc.armedStay[partition] ? PartitionStatus::ARMED_STAY : PartitionStatus::ARMED_AWAY; + notifyPartitionStatusChange(partition, status); + dsc.exitDelayChanged[partition] = false; + } + } + + if (dsc.exitDelayChanged[partition]) { + dsc.exitDelayChanged[partition] = false; + if (dsc.exitDelay[partition]) { + notifyPartitionStatusChange(partition, PartitionStatus::EXIT_DELAY); + } else if (!dsc.armed[partition]) { + updateDisarmed = true; + } + } + + if (updateDisarmed && !dsc.armed[partition] && !dsc.exitDelay[partition]) { + notifyPartitionStatusChange(partition, PartitionStatus::DISARMED); + } + + if (dsc.fireChanged[partition] && fireStatusChangeCallback) { + dsc.fireChanged[partition] = false; + fireStatusChangeCallback(partition + 1, dsc.fire[partition]); + } + + if (dsc.lights[partition] != previousLights[partition] && lightStatusChangeCallback) { + previousLights[partition] = dsc.lights[partition]; + lightStatusChangeCallback(partition + 1, dsc.lights[partition]); + } + } + + // Publishes zones 1-64 status in a separate topic per zone + // Zone status is stored in the openZones[] and openZonesChanged[] arrays using 1 bit per zone, up to 64 zones: + // openZones[0] and openZonesChanged[0]: Bit 0 = Zone 1 ... Bit 7 = Zone 8 + // openZones[1] and openZonesChanged[1]: Bit 0 = Zone 9 ... Bit 7 = Zone 16 + // ... + // openZones[7] and openZonesChanged[7]: Bit 0 = Zone 57 ... Bit 7 = Zone 64 + if (dsc.openZonesStatusChanged && zoneStatusChangeCallback) { + dsc.openZonesStatusChanged = false; + zone = 0; + for (byte zoneGroup = 0; zoneGroup < dscZones; zoneGroup++) { + for (byte zoneBit = 0; zoneBit < 8; zoneBit++) { + zone++; + if (!bitRead(dsc.openZonesChanged[zoneGroup], zoneBit)) { + continue; + } + bool status = bitRead(dsc.openZones[zoneGroup], zoneBit); + zoneStatusChangeCallback(zone, status); + } + dsc.openZonesChanged[zoneGroup] = 0; + } + } + + // Zone alarm status is stored in the alarmZones[] and alarmZonesChanged[] arrays using 1 bit per zone, up to 64 + // zones + // alarmZones[0] and alarmZonesChanged[0]: Bit 0 = Zone 1 ... Bit 7 = Zone 8 + // alarmZones[1] and alarmZonesChanged[1]: Bit 0 = Zone 9 ... Bit 7 = Zone 16 + // ... + // alarmZones[7] and alarmZonesChanged[7]: Bit 0 = Zone 57 ... Bit 7 = Zone 64 + if (dsc.alarmZonesStatusChanged && zoneAlarmChangeCallback) { + dsc.alarmZonesStatusChanged = false; + zone = 0; + for (byte zoneGroup = 0; zoneGroup < dscZones; zoneGroup++) { + for (byte zoneBit = 0; zoneBit < 8; zoneBit++) { + zone++; + if (bitRead(dsc.alarmZonesChanged[zoneGroup], zoneBit)) { + bool status = bitRead(dsc.alarmZones[zoneGroup], zoneBit); + zoneAlarmChangeCallback(zone, status); + } + } + dsc.alarmZonesChanged[zoneGroup] = 0; + } + } + + if (dsc.timestampChanged && panelTimeChangeCallback) { + dsc.timestampChanged = false; + if (dsc.year >= 0 && dsc.year <= 9999 && dsc.month > 0 && dsc.month < 13 && dsc.day > 0 && dsc.day < 32 && + dsc.hour >= 0 && dsc.hour < 24 && dsc.minute >= 0 && dsc.minute < 60) { + std::string time = str_sprintf("%04d-%02d-%02d %02d:%02d", dsc.year, dsc.month, dsc.day, dsc.hour, dsc.minute); + panelTimeChangeCallback(time); + } + } + } + + const __FlashStringHelper *statusText(uint8_t statusCode) { + switch (statusCode) { + case 0x01: + return F("Ready"); + case 0x02: + return F("Stay zones open"); + case 0x03: + return F("Zones open"); + case 0x04: + return F("Armed stay"); + case 0x05: + return F("Armed away"); + case 0x06: + return F("No entry delay"); + case 0x07: + return F("Failed to arm"); + case 0x08: + return F("Exit delay"); + case 0x09: + return F("No entry delay"); + case 0x0B: + return F("Quick exit"); + case 0x0C: + return F("Entry delay"); + case 0x0D: + return F("Alarm memory"); + case 0x10: + return F("Keypad lockout"); + case 0x11: + return F("Alarm"); + case 0x14: + return F("Auto-arm"); + case 0x15: + return F("Arm with bypass"); + case 0x16: + return F("No entry delay"); + case 0x17: + return F("Power failure"); //??? not sure + case 0x22: + return F("Alarm memory"); + case 0x33: + return F("Busy"); + case 0x3D: + return F("Disarmed"); + case 0x3E: + return F("Disarmed"); + case 0x40: + return F("Keypad blanked"); + case 0x8A: + return F("Activate zones"); + case 0x8B: + return F("Quick exit"); + case 0x8E: + return F("Invalid option"); + case 0x8F: + return F("Invalid code"); + case 0x9E: + return F("Enter * code"); + case 0x9F: + return F("Access code"); + case 0xA0: + return F("Zone bypass"); + case 0xA1: + return F("Trouble menu"); + case 0xA2: + return F("Alarm memory"); + case 0xA3: + return F("Door chime on"); + case 0xA4: + return F("Door chime off"); + case 0xA5: + return F("Master code"); + case 0xA6: + return F("Access codes"); + case 0xA7: + return F("Enter new code"); + case 0xA9: + return F("User function"); + case 0xAA: + return F("Time and Date"); + case 0xAB: + return F("Auto-arm time"); + case 0xAC: + return F("Auto-arm on"); + case 0xAD: + return F("Auto-arm off"); + case 0xAF: + return F("System test"); + case 0xB0: + return F("Enable DLS"); + case 0xB2: + return F("Command output"); + case 0xB7: + return F("Installer code"); + case 0xB8: + return F("Enter * code"); + case 0xB9: + return F("Zone tamper"); + case 0xBA: + return F("Zones low batt."); + case 0xC6: + return F("Zone fault menu"); + case 0xC8: + return F("Service required"); + case 0xD0: + return F("Keypads low batt"); + case 0xD1: + return F("Wireless low bat"); + case 0xE4: + return F("Installer menu"); + case 0xE5: + return F("Keypad slot"); + case 0xE6: + return F("Input: 2 digits"); + case 0xE7: + return F("Input: 3 digits"); + case 0xE8: + return F("Input: 4 digits"); + case 0xEA: + return F("Code: 2 digits"); + case 0xEB: + return F("Code: 4 digits"); + case 0xEC: + return F("Input: 6 digits"); + case 0xED: + return F("Input: 32 digits"); + case 0xEE: + return F("Input: option"); + case 0xF0: + return F("Function key 1"); + case 0xF1: + return F("Function key 2"); + case 0xF2: + return F("Function key 3"); + case 0xF3: + return F("Function key 4"); + case 0xF4: + return F("Function key 5"); + case 0xF8: + return F("Keypad program"); + case 0xFF: + return F("Disabled"); + default: + return F("Unknown"); + } + } +}; diff --git a/package.json b/package.json deleted file mode 100644 index 2f047e3..0000000 --- a/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "dscalarm-mqtt", - "description": "DSC Alarm to MQTT bridge using Homie Convention", - "main": "main.cpp", - "repository": { - "type": "git", - "url": "https://github.com/jimtng/dscalarm-mqtt.git" - }, - "keywords": [ - "DSC Alarm", - "dscKeybusInterface", - "Homie", - "homeatomation", - "alarm", - "iot" - ], - "dependencies": [ - "homie-esp8266": "https://github.com/homieiot/homie-esp8266.git#v2.0.0", - "dscKeybusInterface": "https://github.com/taligentx/dscKeybusInterface.git#v1.2" - ], - "frameworks": "arduino", - "platforms": "espressif8266", - "author": "jimtng", - "bugs": { - "url": "https://github.com/jimtng/dscalarm-mqtt/issues" - }, - "homepage": "https://github.com/jimtng/dscalarm-mqtt" - } - \ No newline at end of file diff --git a/platformio.ini b/platformio.ini deleted file mode 100644 index 241f3da..0000000 --- a/platformio.ini +++ /dev/null @@ -1,22 +0,0 @@ -; PlatformIO Project Configuration File -; -; Build options: build flags, source filter -; Upload options: custom upload port, speed and extra flags -; Library options: dependencies, extra library storages -; Advanced options: extra scripting -; -; Please visit documentation for the other options and examples -; https://docs.platformio.org/page/projectconf.html - -;[env:nodemcu] -[env:d1_mini_pro] -platform = espressif8266 -;board = nodemcu -board = d1_mini_pro -framework = arduino -lib_deps = - https://github.com/taligentx/dscKeybusInterface.git#develop - https://github.com/homieiot/homie-esp8266.git#develop-v3 -build_flags = -D PIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY -monitor_speed = 115200 - diff --git a/secret.yaml.example b/secret.yaml.example new file mode 100644 index 0000000..78e7ca4 --- /dev/null +++ b/secret.yaml.example @@ -0,0 +1,13 @@ +# Example secret file - save it as secret.yaml +wifi_ssid: SSID +wifi_password: WIFIPASSWORD +fallback_hotspot_password: "hotspotpassword" + +ota_password: your-ota-password + +web_login: your-weblogin +web_password: your-web-password + +mqtt_broker: your-mqtt-broker + +alarm_access_code: "1234" diff --git a/src/main.cpp b/src/main.cpp deleted file mode 100644 index ef2260f..0000000 --- a/src/main.cpp +++ /dev/null @@ -1,422 +0,0 @@ -/* - * dscKeybusInterface implementation using Homie-esp8266 library. - * - * Author/Github: https://github.com/jimtng - * - * dscKeybusInterface: https://github.com/taligentx/dscKeybusInterface - * Homie-esp8266 (v2.0.0): https://github.com/homieiot/homie-esp8266 - * - * Processes the security system status and implement the Homie convention. - * - * See README.md - * - * For details on the wiring, please see: - * https://github.com/taligentx/dscKeybusInterface - * - * - */ - -#include -#include -#include -#include - -#define SOFTWARE_VERSION "2.0.0 Build 1" - -// Configures the Keybus interface with the specified pins -// dscWritePin is optional, leaving it out disables the virtual keypad. -#define dscClockPin D1 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscReadPin D2 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscWritePin D8 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); - -enum ArmType { arm_away, arm_stay }; - -HomieNode alarmNode("alarm", "alarm", "alarm"); -HomieNode *partitionNode[dscPartitions]; -char partitionNames[dscPartitions][13]; // to store partition names for HomieNode partitions - -HomieSetting dscAccessCode("access-code", "Alarm access code"); - -unsigned long dscStopTime = 0; // the time (in millis) when the dsc-stop was last requested - -byte previousLights[dscPartitions] /*, TODO previousStatus[dscPartitions] */; -const String bit2str[] = {"OFF", "ON"}; -const char* flagMap[] = { "0", "1" }; - -const String lightsToJson(const byte lights) { - String output; - const size_t capacity = JSON_OBJECT_SIZE(8); - const char* lightNames[] = { "ready", "armed", "memory", "bypass", "trouble", "program", "fire", "backlight" }; -#if ARDUINOJSON_VERSION_MAJOR >= 6 - DynamicJsonDocument doc(capacity); - UNTESTED -#else - DynamicJsonBuffer jb(capacity); // staticjsonbuffer doesn't seem to work, producing only 6 objects instead of 8 - JsonObject& doc = jb.createObject(); -#endif - for (byte i = 0; i < 8; i++) { - doc[lightNames[i]] = bit2str[bitRead(lights, i)]; - } -#if ARDUINOJSON_VERSION_MAJOR >= 6 - serializeJson(doc, output); -#else - doc.printTo(output); -#endif - return output; -} - -void resetStatus() { - // we don't want to reset the status of zone changes, which is done by dsc.resetStatus() - dsc.statusChanged = true; - dsc.keybusChanged = true; - dsc.troubleChanged = true; - dsc.powerChanged = true; - dsc.batteryChanged = true; - for (byte partition = 0; partition < dscPartitions; partition++) { - dsc.readyChanged[partition] = true; - dsc.armedChanged[partition] = true; - dsc.alarmChanged[partition] = true; - dsc.fireChanged[partition] = true; - previousLights[partition] = /* TODO previousStatus[i] = */ 0xFF; - } -} - -void setupHandler() { - resetStatus(); - dsc.begin(); -} - -void loopHandler() { - // automatically restart dsc after 5 minutes if it was stopped - if (dscStopTime > 0 && (millis() - dscStopTime) > 5*60*1000) { - dsc.begin(); - dscStopTime = 0; - alarmNode.setProperty("message").setRetained(false).send("DSC Interface restarted automatically"); - } - - dsc.loop(); - if (!dsc.statusChanged || !dsc.keybusConnected) { - return; - } - dsc.statusChanged = false; - - // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // handlePanel() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h - if (dsc.bufferOverflow) { - dsc.bufferOverflow = false; - alarmNode.setProperty("message").setRetained(false).send("Keybus buffer overflow"); - } - - // Sends the access code when needed by the panel for arming - if (dsc.accessCodePrompt) { - dsc.accessCodePrompt = false; - dsc.write(dscAccessCode.get()); - } - - if (dsc.troubleChanged) { - dsc.troubleChanged = false; - alarmNode.setProperty("trouble").send(flagMap[dsc.trouble]); - } - - if (dsc.powerChanged) { - dsc.powerChanged = false; - alarmNode.setProperty("power-trouble").send(flagMap[dsc.powerTrouble]); - } - - if (dsc.batteryChanged) { - dsc.batteryChanged = false; - alarmNode.setProperty("battery-trouble").send(flagMap[dsc.batteryTrouble]); - } - - if (dsc.keypadFireAlarm) { - dsc.keypadFireAlarm = false; - alarmNode.setProperty("fire-alarm-keypad").send("1"); - } - - if (dsc.keypadAuxAlarm) { - dsc.keypadAuxAlarm = false; - alarmNode.setProperty("aux-alarm-keypad").send("1"); - } - - if (dsc.keypadPanicAlarm) { - dsc.keypadPanicAlarm = false; - alarmNode.setProperty("panic-alarm-keypad").send("1"); - } - - if (dsc.timestampChanged) { - dsc.timestampChanged = false; - if (!(dsc.year < 0 || dsc.year > 9999 || dsc.month < 1 || dsc.month > 12 || dsc.day < 1 || dsc.day > 31 || dsc.hour < 0 || dsc.hour > 23 || dsc.minute < 0 || dsc.minute > 59)) { - char panelTime[17]; // YYYY-MM-DD HH:mm - sprintf(panelTime, "%04d-%02d-%02d %02d:%02d", dsc.year, dsc.month, dsc.day, dsc.hour, dsc.minute); - alarmNode.setProperty("panel-time").setRetained(false).send(panelTime); - } - } - - String prefix; - // loop through partitions - for (byte partition = 0; partition < dscPartitions; partition++) { - HomieNode *node = partitionNode[partition]; - if (node == nullptr || dsc.status[partition] == 0xC7 || dsc.status[partition] == 0) { // the partition is disabled https://github.com/taligentx/dscKeybusInterface/issues/99 - continue; - } - - // Publish exit delay status - if (dsc.exitDelayChanged[partition]) { - dsc.exitDelayChanged[partition] = false; - node->setProperty("exit-delay").send(flagMap[dsc.exitDelay[partition]]); - } - - // Publish entry delay status - if (dsc.entryDelayChanged[partition]) { - dsc.entryDelayChanged[partition] = false; - node->setProperty("entry-delay").send(flagMap[dsc.entryDelay[partition]]); - } - - // Publish armed status - if (dsc.armedChanged[partition]) { - dsc.armedChanged[partition] = false; - node->setProperty("away").send(flagMap[dsc.armedAway[partition]]); - node->setProperty("stay").send(flagMap[dsc.armedStay[partition]]); - } - - // Publish alarm status - if (dsc.alarmChanged[partition]) { - dsc.alarmChanged[partition] = false; - node->setProperty("alarm").send(flagMap[dsc.alarm[partition]]); - } - - // Publish fire alarm status - if (dsc.fireChanged[partition]) { - dsc.fireChanged[partition] = false; - node->setProperty("fire").send(flagMap[dsc.fire[partition]]); - } - - // Publish access code - if (dsc.accessCodeChanged[partition]) { - dsc.accessCodeChanged[partition] = false; - String msg; - switch (dsc.accessCode[partition]) { - case 33: - case 34: msg = "duress"; break; - case 40: msg = "master"; break; - case 41: - case 42: msg = "supervisor"; break; - default: msg = "user"; - } - node->setProperty("access-code").setRetained(false).send(msg); - } - - // Publish light status as JSON - if (dsc.lights[partition] != previousLights[partition]) { - previousLights[partition] = dsc.lights[partition]; - node->setProperty("lights").send(lightsToJson(dsc.lights[partition])); - } - - // if (dsc.status[partition] != previousStatus[partition]) { - // previousStatus[partition] = dsc.status[partition]; - // TODO - // } - } - - // Publish zones 1-64 status - // Zone status is stored in the openZones[] and openZonesChanged[] arrays using 1 bit per zone, up to 64 zones: - // openZones[0] and openZonesChanged[0]: Bit 0 = Zone 1 ... Bit 7 = Zone 8 - // openZones[1] and openZonesChanged[1]: Bit 0 = Zone 9 ... Bit 7 = Zone 16 - // ... - // openZones[7] and openZonesChanged[7]: Bit 0 = Zone 57 ... Bit 7 = Zone 64 - if (dsc.openZonesStatusChanged) { - dsc.openZonesStatusChanged = false; // Reset the open zones status flag - for (byte zoneGroup = 0; zoneGroup < dscZones; zoneGroup++) { - for (byte zoneBit = 0; zoneBit < 8; zoneBit++) { - if (bitRead(dsc.openZonesChanged[zoneGroup], zoneBit)) { // Checks an individual open zone status flag - prefix = "openzone-" + String(zoneBit + 1 + (zoneGroup * 8)); - alarmNode.setProperty(prefix).setRetained(false).send(bitRead(dsc.openZones[zoneGroup], zoneBit) ? "1" : "0"); - } - } - dsc.openZonesChanged[zoneGroup] = 0; // reset the changed flags - } - } - - // Publish alarm zones 1-64 - // Zone alarm status is stored in the alarmZones[] and alarmZonesChanged[] arrays using 1 bit per zone, up to 64 zones - // alarmZones[0] and alarmZonesChanged[0]: Bit 0 = Zone 1 ... Bit 7 = Zone 8 - // alarmZones[1] and alarmZonesChanged[1]: Bit 0 = Zone 9 ... Bit 7 = Zone 16 - // ... - // alarmZones[7] and alarmZonesChanged[7]: Bit 0 = Zone 57 ... Bit 7 = Zone 64 - if (dsc.alarmZonesStatusChanged) { - dsc.alarmZonesStatusChanged = false; // Resets the alarm zones status flag - for (byte zoneGroup = 0; zoneGroup < dscZones; zoneGroup++) { - for (byte zoneBit = 0; zoneBit < 8; zoneBit++) { - if (bitRead(dsc.alarmZonesChanged[zoneGroup], zoneBit)) { // Checks an individual alarm zone status flag - prefix = "alarmzone-" + String(zoneBit + 1 + (zoneGroup * 8)); - alarmNode.setProperty(prefix).send(flagMap[bitRead(dsc.alarmZones[zoneGroup], zoneBit)]); - } - } - dsc.alarmZonesChanged[zoneGroup] = 0; - } - } -} - -bool onKeypad(const HomieRange& range, const String& command) { - // we need to have a static buffer because currently dscKeybusInterface dsc.write() - // works directly with the passed string in inside its interrupt handler, - // so the string given to dsc.write needs to remain in memory - static char commandBuffer[64]; - if (command.length() >= sizeof(commandBuffer)/sizeof(commandBuffer[0])) { - alarmNode.setProperty("message").setRetained(false).send("The keypad data is too long. Max: " + String(sizeof(commandBuffer)-1)); - } else { - unsigned long t = millis(); - while (!dsc.writeReady) { - dsc.loop(); // wait until all pending writes are done - if (millis() - t > 10000) { // don't wait forever - max 10s - alarmNode.setProperty("message").setRetained(false).send("ERROR: Timed out"); - return true; - } - } - strcpy(commandBuffer, command.c_str()); - dsc.write(commandBuffer); - } - return true; -} - -// Arm - the partition argument is 1 based -void arm(byte partition, ArmType armType = arm_away) { - if (!dsc.armed[partition - 1] && !dsc.exitDelay[partition - 1]) { // Read the argument sent from the homey flow - dsc.writePartition = partition; - dsc.write(armType == arm_away ? 'w' : 's'); - } -} - -// Disarm - the partition argument is 1 based -void disarm(byte partition) { - if ((dsc.armed[partition - 1] || dsc.exitDelay[partition - 1])) { - dsc.writePartition = partition; // Sets writes to the partition number - dsc.write(dscAccessCode.get()); - } -} - -bool onRefreshStatus(const HomieRange& range, const String& command) { - resetStatus(); - alarmNode.setProperty("refresh-status").setRetained(false).send("OK"); - return true; -} - -bool onMaintenance(const HomieRange& range, const String& command) { - if (command == "reboot") { - alarmNode.setProperty("message").setRetained(false).send("Rebooting..."); - Homie.reboot(); - } else if (command == "dsc-stop") { - dsc.stop(); - dscStopTime = millis(); - alarmNode.setProperty("message").setRetained(false).send("DSC Interface stopped"); - } else if (command == "dsc-start") { - if (dscStopTime > 0) { - dscStopTime = 0; - dsc.begin(); - } - alarmNode.setProperty("message").setRetained(false).send("DSC Interface started"); - } else { - alarmNode.setProperty("message").setRetained(false).send("Unknown maintenance command"); - return false; - } - return true; -} - -// parse the provided time and set the dsc panel time -bool onPanelTime(const HomieRange& range, const String& command) { - if (dsc.keybusConnected) { - struct tm tm; // note tm_year is year since 1900, and tm_mon is month since January. Add offsets accordingly - strptime(command.c_str(), "%Y-%m-%d %H:%M", &tm); - dsc.setTime(1900 + tm.tm_year, 1 + tm.tm_mon, tm.tm_mday, tm.tm_hour, tm.tm_min, dscAccessCode.get()); - alarmNode.setProperty("message").setRetained(false).send("OK"); - } else { - alarmNode.setProperty("message").setRetained(false).send("ERROR: DSC Keybus is not connected"); - } - return true; -} - -void onHomieEvent(const HomieEvent& event) { - switch (event.type) { - case HomieEventType::MQTT_READY: resetStatus(); break; - case HomieEventType::OTA_STARTED: dsc.stop(); break; - // case HomieEventType::OTA_SUCCESSFUL: - // case HomieEventType::OTA_FAILED: dsc.begin(); break; - default: break; // to silence compiler warning - } -} - -void setup() { - Serial.begin(115200); - - Homie_setFirmware("dsc-alarm", SOFTWARE_VERSION); - Homie.disableResetTrigger(); - Homie.setSetupFunction(setupHandler).setLoopFunction(loopHandler); - Homie.onEvent(onHomieEvent); - - alarmNode.advertise("refresh-status").settable(onRefreshStatus); - alarmNode.advertise("maintenance").settable(onMaintenance); - alarmNode.advertise("keypad").settable(onKeypad); // write to dsc alarm - alarmNode.advertise("trouble"); - alarmNode.advertise("power-trouble"); - alarmNode.advertise("battery-trouble"); - alarmNode.advertise("fire-alarm-keypad"); - alarmNode.advertise("aux-alarm-keypad"); - alarmNode.advertise("panic-alarm-keypad"); - alarmNode.advertise("panel-time").settable(onPanelTime); - alarmNode.advertise("debug").settable([](const HomieRange &range, const String &value) { - for (byte i = 0; i < dscPartitions; i++) { - alarmNode.setProperty("debug").setRetained(false).send(String(i) + ": " + String(dsc.status[i], HEX)); - } - return true; - }); - - dsc.begin(); - for (int i = 0; i < 3*10; i++) { dsc.loop(); delay(100); }; - - for (byte i = 0; i < dscPartitions; i++) { - if (dsc.status[i] == 0 || dsc.status[i] == 0xC7) { - partitionNode[i] = nullptr; - Serial << "Partition " + String(i+1) + " inactive" << endl; - continue; - } - - char istr[3]; - strcpy(partitionNames[i], "partition-"); - if (i < 99) { // avoid the highly unlikely buffer overflow - strcat(partitionNames[i], itoa(i+1, istr, 10)); - } - HomieNode *node = new HomieNode(partitionNames[i], partitionNames[i], "partition"); // node id/names must remain allocated as HomieNode makes no copy of it - partitionNode[i] = node; - node->advertise("away").settable([i](const HomieRange &range, const String &value) { - if (value == "1" || value.equalsIgnoreCase("on") || value.equalsIgnoreCase("arm")) { - arm(i+1, arm_away); - } else { - disarm(i+1); - } - return true; - }); - node->advertise("stay").settable([i](const HomieRange &range, const String &value) { - if (value == "1" || value.equalsIgnoreCase("on") || value.equalsIgnoreCase("arm")) { - arm(i+1, arm_stay); - } else { - disarm(i+1); - } - return true; - }); - node->advertise("alarm"); - node->advertise("exit-delay"); - node->advertise("entry-delay"); - node->advertise("fire"); - node->advertise("access-code"); - } - dsc.stop(); // we need to stop it before Homie.setup(), and starts/begins again inside Homie's setupHandler(). - - Homie.setup(); -} - -void loop() { - Homie.loop(); -} - -