Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for virtual zones #17

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
ARG BUILD_FROM=ghcr.io/hassio-addons/base:15.0.9
# hadolint ignore=DL3006
FROM ${BUILD_FROM}

# Set shell
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

# Copy Python requirements file
COPY requirements.txt /tmp/

# Setup base
# hadolint ignore=DL3003,DL3042
RUN \
apk add --no-cache --virtual .build-dependencies \
build-base=0.5-r3 \
python3-dev=3.11.10-r0 \
\
&& apk add --no-cache \
py3-pip=23.3.1-r0 \
py3-wheel=0.42.0-r0 \
python3=3.11.10-r0 \
\
&& pip install -r /tmp/requirements.txt \
\
&& cd /usr/lib/python3.11/site-packages/ \
\
&& find /usr \
\( -type d -a -name test -o -name tests -o -name '__pycache__' \) \
-o \( -type f -a -name '*.pyc' -o -name '*.pyo' \) \
-exec rm -rf '{}' + \
\
&& apk del --no-cache --purge .build-dependencies

# Copy root filesystem
COPY rootfs /
# Copy addon code and entrypoint
COPY ad-mqtt /root/
COPY run-addon.py /root/run-addon.py

# Build arguments
ARG BUILD_ARCH
ARG BUILD_DATE
ARG BUILD_DESCRIPTION
ARG BUILD_NAME
ARG BUILD_REF
ARG BUILD_REPOSITORY
ARG BUILD_VERSION

# Labels
LABEL \
io.hass.name="${BUILD_NAME}" \
io.hass.description="${BUILD_DESCRIPTION}" \
io.hass.arch="${BUILD_ARCH}" \
io.hass.type="addon" \
io.hass.version=${BUILD_VERSION} \
org.opencontainers.image.title="${BUILD_NAME}" \
org.opencontainers.image.description="${BUILD_DESCRIPTION}" \
org.opencontainers.image.source="https://github.com/${BUILD_REPOSITORY}" \
org.opencontainers.image.created=${BUILD_DATE} \
org.opencontainers.image.revision=${BUILD_REF} \
org.opencontainers.image.version=${BUILD_VERSION}
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,8 @@ Designed to work with Home Assistant. Uses MQTT discovery to create sensors
for all zones and the alarm panel automatically. Times are passed in the
MQTT messages and retained so they can be used to create real "last changed"
time sensors if desired in HASS.

If you use virtual zones, you need to enable the corresponding zone expander
emulation in the Alarm Decoder setup. Then, you can include `is_virtual=True`
in a Zone configuration, and it will get a switch that can be used to trigger
the emulated zone fault.
27 changes: 27 additions & 0 deletions ad_mqtt/Bridge.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import datetime as DT
import json
import logging
import functools

LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -37,6 +38,8 @@ def __init__(self, mqtt, decoder, code, zones, rf_devices):

self.sensor_state_topic = "alarm/sensor/{entity}/state"
self.sensor_battery_topic = "alarm/sensor/{entity}/battery"
self.virtual_zone_state_topic = "alarm/sensor/{entity}/virtual/state"
self.virtual_zone_set_topic = "alarm/sensor/{entity}/virtual/set"

mqtt.signal_connected.connect(self.mqtt_connected)

Expand All @@ -52,6 +55,30 @@ def mqtt_connected(self, device, connected):
self.mqtt.subscribe(self.panel_set_topic, qos, self.cb_panel_set)
self.mqtt.subscribe(self.chime_set_topic, qos, self.cb_chime_set)
self.mqtt.subscribe(self.bypass_set_topic, qos, self.cb_bypass_set)
for z in self.zones.values():
if z.is_virtual:
fault_entity = z.entity + '_fault'
fault_unique_id = z.unique_id + "_fault"
set_topic = self.virtual_zone_set_topic.format(
unique_id=fault_entity, entity=fault_unique_id)
self.mqtt.subscribe(set_topic, qos, functools.partial(self.cb_virtual_fault_set, zone=z))

def cb_virtual_fault_set(self, client, user_data, message, zone):
topic = message.topic
msg = message.payload.decode("utf-8").strip()
is_faulted = (msg.lower() == "on")
LOG.info("Read virtual fault set '%s': '%s', will mark zone %s as %s", topic, msg, zone.zone, is_faulted)

payload = {"status" : "ON" if is_faulted else "OFF"}
fault_entity = zone.entity + '_fault'
fault_unique_id = zone.unique_id + "_fault"
state_topic = self.virtual_zone_state_topic.format(
unique_id=fault_entity, entity=fault_unique_id)
self.publish(state_topic, {}, payload)
if is_faulted:
self.ad.fault_zone(zone.zone)
else:
self.ad.clear_zone(zone.zone)

def cb_panel_set(self, client, user_data, message):
msg = message.payload.decode("utf-8")
Expand Down
4 changes: 3 additions & 1 deletion ad_mqtt/Devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ def init_devices(devices):


class Zone:
def __init__(self, zone, entity, label, device_class=None):
def __init__(self, zone, entity, label, device_class=None, is_virtual=False):
self.zone = zone # int zone number
self.entity = entity
self.label = label
self.faulted = None # None=Unknown, True=faulted, False=clear
self.has_battery = False
self.unique_id = None
self.is_virtual = is_virtual
self.device_class = device_class if device_class else \
guess_class((entity, label))

Expand All @@ -47,6 +48,7 @@ def __init__(self, id, zone, entity, label):
"fire" : "smoke",
"door" : "door",
"window" : "window",
"heat": "heat",
}


Expand Down
23 changes: 23 additions & 0 deletions ad_mqtt/Discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,29 @@ def __init__(self, mqtt, bridge, zones):
}
self.messages.append((topic, payload))

if z.is_virtual:
fault_entity = z.entity + '_fault'
fault_unique_id = z.unique_id + "_fault"
topic = f'homeassistant/switch/{fault_unique_id}/config'
state_topic = bridge.virtual_zone_state_topic.format(
unique_id=fault_entity, entity=fault_unique_id)
set_topic = bridge.virtual_zone_set_topic.format(
unique_id=fault_entity, entity=fault_unique_id)
payload = {
'name' : z.label + ' Fault',
'object_id' : fault_entity,
'unique_id' : fault_unique_id,
'device' : Discovery.device,
'state_topic' : state_topic,
'value_template' : '{{value_json.status}}',
'json_attributes_topic' : state_topic,
'json_attributes_template' : attr_templ,
'command_topic' : set_topic,
'qos' : 1,
'retain' : True,
}
self.messages.append((topic, payload))

def mqtt_connected(self, device, connected):
if not self.messages:
return
Expand Down
26 changes: 26 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
name: ad-mqtt
version: dev
slug: ad-mqtt
description: MQTT Bridge for AlarmDecoder
url: https://github.com/dgrnbrg/ad-mqtt
arch:
- aarch64
- amd64
- armv7
init: false
startup: application
services:
- mqtt:need
options:
system_packages: []
python_packages: []
init_commands: []
schema:
log_level: list(trace|debug|info|notice|warning|error|fatal)?
system_packages:
- str
python_packages:
- str
init_commands:
- str
Empty file.
27 changes: 27 additions & 0 deletions rootfs/etc/s6-overlay/s6-rc.d/ad-mqtt/finish
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/command/with-contenv bashio
# shellcheck shell=bash
# ==============================================================================
# Home Assistant Community Add-on: ad-mqtt
# Take down the S6 supervision tree when ad-mqtt fails
# ==============================================================================
declare exit_code
readonly exit_code_container=$(</run/s6-linux-init-container-results/exitcode)
readonly exit_code_service="${1}"
readonly exit_code_signal="${2}"
readonly service="ad-mqtt"

bashio::log.info \
"Service ${service} exited with code ${exit_code_service}" \
"(by signal ${exit_code_signal})"

if [[ "${exit_code_service}" -eq 256 ]]; then
if [[ "${exit_code_container}" -eq 0 ]]; then
echo $((128 + $exit_code_signal)) > /run/s6-linux-init-container-results/exitcode
fi
[[ "${exit_code_signal}" -eq 15 ]] && exec /run/s6/basedir/bin/halt
elif [[ "${exit_code_service}" -ne 0 ]]; then
if [[ "${exit_code_container}" -eq 0 ]]; then
echo "${exit_code_service}" > /run/s6-linux-init-container-results/exitcode
fi
exec /run/s6/basedir/bin/halt
fi
39 changes: 39 additions & 0 deletions rootfs/etc/s6-overlay/s6-rc.d/ad-mqtt/run
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/command/with-contenv bashio
# shellcheck shell=bash
# ==============================================================================
# Home Assistant Community Add-on: ad-mqtt
# Runs the ad-mqtt
# ==============================================================================
declare log_level

bashio::log.info "Starting ad-mqtt..."

# Find the matching Tor log level
log_level="INFO"
if bashio::config.has_value 'log_level'; then
case "$(bashio::string.lower "$(bashio::config 'log_level')")" in
all|trace|debug)
log_level="DEBUG"
;;
info|notice)
log_level="INFO"
;;
warning)
log_level="WARNING"
;;
error)
log_level="ERROR"
;;
fatal|off)
log_level="FATAL"
;;
esac
fi

export MQTT_HOST=$(bashio::services mqtt "host")
export MQTT_PORT=$(bashio::services mqtt "port")
export MQTT_USER=$(bashio::services mqtt "username")
export MQTT_PASSWORD=$(bashio::services mqtt "password")

# Run the ad-mqtt
exec python /root/run.py
1 change: 1 addition & 0 deletions rootfs/etc/s6-overlay/s6-rc.d/ad-mqtt/type
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
longrun
Empty file.
21 changes: 21 additions & 0 deletions rootfs/etc/s6-overlay/s6-rc.d/init-ad-mqtt/run
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/command/with-contenv bashio
# shellcheck shell=bash
# ==============================================================================
# Home Assistant Community Add-on: ad-mqtt
# Configures ad-mqtt
# ==============================================================================

# Migrate add-on data from the Home Assistant config folder,
# to the add-on configuration folder.
if ! bashio::fs.file_exists '/config/appdaemon.yaml' \
&& bashio::fs.file_exists '/homeassistant/appdaemon/appdaemon.yaml'; then
shopt -s dotglob
mv /homeassistant/appdaemon/* /config/ \
|| bashio::exit.nok "Failed to migrate AppDaemon configuration"
fi

# Creates initial AppDaemon configuration in case it is non-existing
if ! bashio::fs.file_exists '/config/appdaemon.yaml'; then
cp -R /root/appdaemon/* /config/ \
|| bashio::exit.nok 'Failed to create initial AppDaemon configuration'
fi
1 change: 1 addition & 0 deletions rootfs/etc/s6-overlay/s6-rc.d/init-ad-mqtt/type
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
oneshot
1 change: 1 addition & 0 deletions rootfs/etc/s6-overlay/s6-rc.d/init-ad-mqtt/up
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/etc/s6-overlay/s6-rc.d/init-ad-mqtt/run
Empty file.
Empty file.
14 changes: 14 additions & 0 deletions rootfs/root/appdaemon/appdaemon.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
appdaemon:
latitude: 52.379189
longitude: 4.899431
elevation: 2
time_zone: Europe/Amsterdam
plugins:
HASS:
type: hass
http:
url: http://127.0.0.1:5050
admin:
api:
hadashboard:
4 changes: 4 additions & 0 deletions rootfs/root/appdaemon/apps/apps.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
hello_world:
module: hello
class: HelloWorld
13 changes: 13 additions & 0 deletions rootfs/root/appdaemon/apps/hello.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import appdaemon.plugins.hass.hassapi as hass

#
# Hellow World App
#
# Args:
#

class HelloWorld(hass.Hass):

def initialize(self):
self.log("Hello from AppDaemon")
self.log("You are now ready to run Apps!")
14 changes: 14 additions & 0 deletions rootfs/root/appdaemon/dashboards/Hello.dash
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#
# Main arguments, all optional
#
title: Hello Panel
widget_dimensions: [120, 120]
widget_margins: [5, 5]
columns: 8

label:
widget_type: label
text: Hello World

layout:
- label(2x2)
48 changes: 48 additions & 0 deletions run-addon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import logging
import sys
import os
import json
sys.path.insert( 0, "." )
import ad_mqtt as AD

with open('/data/options.json') as f:
options = json.load(f)

cfg = AD.Config()

# Alarm Decoder ser2sock server location.
cfg.alarm.host = options['alarm']['host']
cfg.alarm.port = options['alarm'].get('port', 10000)
# To reset all zones to closed (not faulted) on startup, set this to True
cfg.alarm.restore_on_startup = options['alarm'].get('restore_on_startup', True)

# MQTT Broker connection
cfg.mqtt.broker = os.env['MQTT_HOST']
cfg.mqtt.port = os.env['MQTT_PORT']
cfg.mqtt.username = os.env['MQTT_USERNAME']
cfg.mqtt.password = os.env['MQTT_PASSWORD']
# Optional encryption settings for the broker.
cfg.mqtt.encryption.ca_cert = None
cfg.mqtt.encryption.certfile = None
cfg.mqtt.encryption.keyfile = None

# Debugging information
cfg.log.level = options['log_level']
cfg.log.screen = True
cfg.log.modules = ["ad_mqtt", "insteon_mqtt"]

# For possible device class values, see:
# https://www.home-assistant.io/integrations/binary_sensor/#device-class
alarm_code = options['alarm']['code']
devices = []
for d in options['devices']:
devices.append(AD.Zone(
zone = d['zone'],
entity = d['entity'],
label = d['label'],
device_class = d.get('device_class'),
is_virtual = d.get('is_virtual', False),
))

AD.run.run(cfg, alarm_code, devices)

Loading