Skip to content

Commit

Permalink
Added leak sensor support. Fixes #6.
Browse files Browse the repository at this point in the history
  • Loading branch information
TD22057 committed Dec 12, 2017
1 parent 5861df1 commit afa2f57
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 46 deletions.
31 changes: 31 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ insteon:
fan_linc:
- 9a.a1.b3

# Leak sensors
leak:

#------------------------------------------------------------------------
# FUTURE: Insteon scene definitions.
#scenes:
Expand Down Expand Up @@ -261,6 +264,7 @@ mqtt:
# dawn/dusk notification which is configured here.
#
# In Home Assistant use MQTT binary sensor with a configuration like:
# binary_sensor:
# - platform: mqtt
# state_topic: 'insteon/aa.bb.cc/dawn'
# device_class: 'light'
Expand All @@ -278,6 +282,33 @@ mqtt:
dawn_dusk_topic: 'insteon/{{address}}/dawn'
dawn_dusk_payload: '{{is_dawn_str.upper()}}'

#------------------------------------------------------------------------
# Leak sensors
#------------------------------------------------------------------------

# Leak sensors will use the state and low battery configuration
# inputs from battery_sensor and add in the leak topic which is
# configured here.
#
# In Home Assistant use MQTT binary sensor with a configuration like:
# binary_sensor:
# - platform: mqtt
# state_topic: 'insteon/aa.bb.cc/leak'
# device_class: 'moisture'
motion:
# Output wet/dry change topic and payload. This message is sent
# whenever the device changes state to wet or dry.
# Available variables for templating are:
# address = 'aa.bb.cc'
# name = 'device name'
# is_wet = 0/1
# is_set_str = 'off', 'on'
# is_dry = 0/1
# is_dry_str = 'off', 'on'
# state = 'wet', 'dry'
wet_dry_topic: 'insteon/{{address}}/wet'
wet_dry_payload: '{{is_wet_str.upper()}}'

#------------------------------------------------------------------------
# Smoke Bridge
#------------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions insteon_mqtt/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
'dimmer' : device.Dimmer,
'battery_sensor' : device.BatterySensor,
'fan_linc' : device.FanLinc,
'leak' : device.Leak,
'mini_remote4' : functools.partial(device.Remote, num=4),
'mini_remote8' : functools.partial(device.Remote, num=8),
'motion' : device.Motion,
Expand Down
68 changes: 35 additions & 33 deletions insteon_mqtt/device/BatterySensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
# Insteon battery powered motion sensor
#
#===========================================================================
from collections import namedtuple
import enum
from .Base import Base
from .. import log
from ..Signal import Signal
Expand All @@ -24,14 +22,6 @@ class BatterySensor(Base):
broadcast group 03 = low battery (0x11) / good battery (0x13)
broadcast group 04 = heartbeat (0x11)
"""
# broadcast group ID alert description
class Type(enum.IntEnum):
ACTIVE = 0x01
LOW_BATTERY = 0x03
HEARTBEAT = 0x04

TypeHandler = namedtuple("TypeHandler", ["signal", "on_arg", "off_arg" ])

def __init__(self, protocol, modem, address, name=None):
"""Constructor
Expand All @@ -49,12 +39,15 @@ def __init__(self, protocol, modem, address, name=None):
self.signal_low_battery = Signal() # (Device, bool)
self.signal_heartbeat = Signal() # (Device, bool)

# Derived classes can override these or add to them. Maps
# Insteon groups to message type for this sensor.
self.group_map = {
self.Type.ACTIVE : self.TypeHandler(self.signal_active, True, False),
self.Type.LOW_BATTERY : self.TypeHandler(self.signal_low_battery,
True, False),
self.Type.HEARTBEAT : self.TypeHandler(self.signal_heartbeat,
True, True),
# General activity on group 1.
0x01 : self.handle_active,
# Low battery is on group 3
0x03 : self.handle_low_battery,
# Heartbeat is on group 4
0x04 : self.handle_heartbeat,
}

self._is_on = False
Expand All @@ -80,8 +73,7 @@ def pair(self, on_done=None):
# groups back to the modem. If one doesn't exist, add it on
# our device and the modem.
add_groups = []
for type in self.Type:
group = type.value
for group in self.group_map:
if not self.db.find(self.modem.addr, group, True):
LOG.info("BatterySensor adding ctrl for group %s", group)
add_groups.append(group)
Expand Down Expand Up @@ -129,39 +121,49 @@ def handle_broadcast(self, msg):
self.refresh(force=True)
return

# On command.
elif msg.cmd1 == 0x11:
LOG.info("BatterySensor %s broadcast ON grp: %s", self.addr,
msg.group)

handler = self.group_map.get(msg.group, None)
if not handler:
LOG.error("BatterySensor no handler for group %s", msg.group)
return

handler.signal.emit(self, handler.on_arg)
# On (0x11) and off (0x13) commands.
elif msg.cmd1 == 0x11 or msg.cmd1 == 0x13:
LOG.info("BatterySensor %s broadcast cmd %s grp: %s", self.addr,
msg.cmd, msg.group)

# Off command.
elif msg.cmd1 == 0x13:
LOG.info("Motion %s broadcast OFF grp: %s", self.addr, msg.group)
handler = self.group_map.get(msg.group, None)
if not handler:
LOG.error("BatterySensor no handler for group %s", msg.group)
return

handler.signal.emit(self, handler.off_arg)
handler(msg)

# Broadcast to the devices we're linked to. Call
# handle_broadcast for any device that we're the controller of.
LOG.debug("BatterySensor %s have db %s", self.addr, len(self.db))
super().handle_broadcast(msg)

# Use this opportunity to get the device db since we know the
# sensor is awake.
LOG.debug("BatterySensor %s have db %s items", self.addr, len(self.db))
if len(self.db) == 0:
LOG.info("BatterySensor %s awake - requesting database", self.addr)
self.refresh(force=True)

#-----------------------------------------------------------------------
def handle_active(self, msg):
"""TODO: doc
"""
self._set_is_on(msg.cmd1 == 0x11)

#-----------------------------------------------------------------------
def handle_low_battery(self, msg):
"""TODO: doc
"""
# Send True for low battery, False for regular.
self.signal_low_battery.emit(msg.cmd1 == 0x11)

#-----------------------------------------------------------------------
def handle_heartbeat(self, msg):
"""TODO: doc
"""
# Send True for any heart beat message
self.signal_heartbeat.emit(True)

#-----------------------------------------------------------------------
def handle_refresh(self, msg, on_done=None):
"""Handle replies to the refresh command.
Expand Down
80 changes: 80 additions & 0 deletions insteon_mqtt/device/Leak.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#===========================================================================
#
# Insteon battery powered motion sensor
#
#===========================================================================
import enum
from .BatterySensor import BatterySensor
from .. import log

LOG = log.get_logger()


class Leak(BatterySensor):
"""Insteon battery powered water leak sensor.
A leak sensor is basically an on/off sensor except that it's
batter powered and only awake when water is detected or the set
button is pressed. It will broadcast an on command for group 1
when dry and on command for group 2 when wet.
The issue with a battery powered sensor is that we can't download
the link database without the sensor being on. You can trigger
the sensor manually and then quickly send an MQTT command with the
payload 'getdb' to download the database. We also can't test to
see if the local database is current or what the current motion
state is - we can really only respond to the sensor when it sends
out a message.
The Signal Leak.signal_active(True) will be emitted whenever the
device senses water and signal_actdive(False) when no water is detected.
TODO: download the database automatically when motion is seen.
"""
# broadcast group ID alert description
class Type(enum.IntEnum):
WET = 0x02

def __init__(self, protocol, modem, address, name=None):
"""Constructor
Args:
protocol: (Protocol) The Protocol object used to communicate
with the Insteon network. This is needed to allow
the device to send messages to the PLM modem.
modem: (Modem) The Insteon modem used to find other devices.
address: (Address) The address of the device.
name (str) Nice alias name to use for the device.
"""
super().__init__(protocol, modem, address, name)

# Leak sensor uses group 1 for dry, group 2 for wet.
self.group_map[0x01] = self.handle_dry
self.group_map[0x02] = self.handle_wet

#-----------------------------------------------------------------------
def handle_dry(self, msg):
"""TODO: doc
"""
# off = dry, on == wet
self._set_is_on(False)

#-----------------------------------------------------------------------
def handle_wet(self, msg):
"""TODO: doc
"""
# off = dry, on == wet
self._set_is_on(True)

#-----------------------------------------------------------------------
def handle_heartbeat(self, msg):
"""TODO: doc
"""
# Update the wet/dry state using the heartbeat if needed.
is_wet = msg.cmd1 == 0x13
if self._is_on != is_wet:
self._set_is_on(is_wet)

super().handle_heartbeat(msg)

#-----------------------------------------------------------------------
21 changes: 9 additions & 12 deletions insteon_mqtt/device/Motion.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# Insteon battery powered motion sensor
#
#===========================================================================
import enum
from .BatterySensor import BatterySensor
from .. import log
from ..Signal import Signal
Expand Down Expand Up @@ -53,15 +52,7 @@ class Motion(BatterySensor):
and save it to file.
refresh: No arguments. Ping the device and see if the database is
current. Reloads the modem database if needed.
"""
# broadcast group ID alert description
class Type(enum.IntEnum):
ACTIVE = 0x01
DAWN = 0x02
LOW_BATTERY = 0x03
HEARTBEAT = 0x04

def __init__(self, protocol, modem, address, name=None):
"""Constructor
Expand All @@ -77,8 +68,14 @@ def __init__(self, protocol, modem, address, name=None):

self.signal_dawn = Signal() # (Device, bool)

self.group_map.update({
self.Type.DAWN : self.TypeHandler(self.signal_dawn, True, False),
})
# Dawn/dusk is on group 02.
self.group_map[0x02] = self.handle_dawn

#-----------------------------------------------------------------------
def handle_dawn(self, msg):
"""TODO: doc
"""
# Send True for dawn, False for dusk.
self.signal_dawn.emit(msg.cmd1 == 0x11)

#-----------------------------------------------------------------------
1 change: 1 addition & 0 deletions insteon_mqtt/device/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from .BatterySensor import BatterySensor
from .Dimmer import Dimmer
from .FanLinc import FanLinc
from .Leak import Leak
from .Motion import Motion
from .Remote import Remote
from .SmokeBridge import SmokeBridge
Expand Down
71 changes: 71 additions & 0 deletions insteon_mqtt/mqtt/Leak.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#===========================================================================
#
# MQTT leak sensor device
#
#===========================================================================
from .. import log
from .BatterySensor import BatterySensor
from .MsgTemplate import MsgTemplate

LOG = log.get_logger()


class Leak(BatterySensor):
"""TODO: doc
This class uses the regular sensor active signal from
BatterySensor for wet/dry events but we have a different method
handle_active below to handle the result vs the one in
BatterySensor.
"""
def __init__(self, mqtt, device):
"""TODO: doc
"""
super().__init__(mqtt, device)

self.msg_wet = MsgTemplate(
topic='insteon/{{address}}/leak',
payload='{{state.upper()}}',
)

#-----------------------------------------------------------------------
def load_config(self, config, qos=None):
"""TODO: doc
"""
super().load_config(config, qos)

data = config.get("leak", None)
if not data:
return

self.msg_wet.load_config(data, 'wet_dry_topic', 'wet_dry_payload', qos)

#-----------------------------------------------------------------------
# pylint: disable=arguments-differ
def handle_active(self, device, is_wet):
"""Device wet/dry on/off callback.
This is triggered via signal when the Insteon device detects
wet or dry. It will publish an MQTT message with the new
state.
Args:
device: (device.Base) The Insteon device that changed.
is_wet: (bool) True for wet, False for dry.
"""
LOG.info("MQTT received wet/dry change %s '%s' wet= %s",
device.addr, device.name, is_wet)

data = {
"address" : device.addr.hex,
"name" : device.name if device.name else device.addr.hex,
"is_wet" : 1 if is_wet else 0,
"is_wet_str" : "on" if is_wet else "off",
"is_dry" : 0 if is_wet else 1,
"is_dry_str" : "off" if is_wet else "on",
"state" : "wet" if is_wet else "dry",
}

self.msg_wet.publish(self.mqtt, data)

#-----------------------------------------------------------------------
1 change: 0 additions & 1 deletion insteon_mqtt/mqtt/Motion.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ def load_config(self, config, qos=None):
self.msg_battery.load_config(data, 'low_battery_topic',
'low_battery_payload', qos)


#-----------------------------------------------------------------------
def handle_dawn(self, device, is_dawn):
"""Device dawn/dusk on/off callback.
Expand Down
Loading

0 comments on commit afa2f57

Please sign in to comment.