Skip to content

Commit

Permalink
Merge pull request #447 from TD22057/dev
Browse files Browse the repository at this point in the history
Version 1.0.2
  • Loading branch information
krkeegan authored Sep 27, 2021
2 parents 28ca2b8 + 1e46c66 commit 2ab12cd
Show file tree
Hide file tree
Showing 12 changed files with 142 additions and 26 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 1.0.1
current_version = 1.0.2
commit = True
tag = False

Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Revision Change History

## [1.0.2]

### Fixes

- Fix problem with heartbeat sensors in HomeAssistant displaying "invalid
timestamp". ([PR 443][P443])
- Fix problem with fan always reporting 'off' state in HomeAssistant.
(thanks to @tstabrawa)([PR 441][P441])
- Fix possible error with KPL switch devices where all leds turn off at once.
([PR 439][P439])
- Fix an error causing the discovery template to advertise the wrong topic
for leak devices. ([PR 444][P444])
- Add support for encrypted MQTT connections using the command line.
([PR 445][P445])

## [1.0.1]

### Additions
Expand Down Expand Up @@ -788,3 +803,8 @@ will add new features.
[P431]: https://github.com/TD22057/insteon-mqtt/pull/431
[P434]: https://github.com/TD22057/insteon-mqtt/pull/434
[P435]: https://github.com/TD22057/insteon-mqtt/pull/435
[P439]: https://github.com/TD22057/insteon-mqtt/pull/439
[P441]: https://github.com/TD22057/insteon-mqtt/pull/441
[P443]: https://github.com/TD22057/insteon-mqtt/pull/443
[P444]: https://github.com/TD22057/insteon-mqtt/pull/444
[P445]: https://github.com/TD22057/insteon-mqtt/pull/445
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ integrated into and controlled from anything that can use MQTT.

This package works well with HomeAssistant and can be easily [installed as an addon](docs/HA_Addon_Instructions.md) using the HomeAssistant Supervisor.

Version: 1.0.1 ([History](CHANGELOG.md))
Version: 1.0.2 ([History](CHANGELOG.md))

### Recent Breaking Changes

Expand Down
2 changes: 1 addition & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "Insteon MQTT",
"description": "Creates an MQTT interface to the Insteon protocol.",
"slug": "insteon-mqtt",
"version": "1.0.1",
"version": "1.0.2",
"startup": "services",
"arch": ["amd64","armhf","aarch64","i386"],
"boot": "auto",
Expand Down
75 changes: 75 additions & 0 deletions insteon_mqtt/cmd_line/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# Command line utilities
#
#===========================================================================
import ssl
import json
import random
import time
Expand All @@ -14,6 +15,40 @@
# there is no pure right answer for what this should be.
TIME_OUT = 30

# map for Paho acceptable TLS cert request options
CERT_REQ_OPTIONS = {'none': ssl.CERT_NONE, 'required': ssl.CERT_REQUIRED}

# Map for Paho acceptable TLS version options. Some options are
# dependent on the OpenSSL install so catch exceptions
TLS_VER_OPTIONS = dict()
try:
TLS_VER_OPTIONS['tls'] = ssl.PROTOCOL_TLS
except AttributeError:
pass
try:
TLS_VER_OPTIONS['tlsv1'] = ssl.PROTOCOL_TLSv1
except AttributeError:
pass
try:
TLS_VER_OPTIONS['tlsv11'] = ssl.PROTOCOL_TLSv1_1
except AttributeError:
pass
try:
TLS_VER_OPTIONS['tlsv12'] = ssl.PROTOCOL_TLSv1_2
except AttributeError:
pass
try:
TLS_VER_OPTIONS['sslv2'] = ssl.PROTOCOL_SSLv2
except AttributeError:
pass
try:
TLS_VER_OPTIONS['sslv23'] = ssl.PROTOCOL_SSLv23
except AttributeError:
pass
try:
TLS_VER_OPTIONS['sslv3'] = ssl.PROTOCOL_SSLv3
except AttributeError:
pass

#===========================================================================
def send(config, topic, payload, quiet=False):
Expand Down Expand Up @@ -46,6 +81,46 @@ def send(config, topic, payload, quiet=False):
password = config["mqtt"].get("password", None)
client.username_pw_set(user, password)

encryption = config["mqtt"].get('encryption', {})
if encryption is None:
encryption = {}
ca_cert = encryption.get('ca_cert', None)
if ca_cert is not None and ca_cert != "":
# Set the basic arguments
certfile = encryption.get('certfile', None)
if certfile == "":
certfile = None
keyfile = encryption.get('keyfile', None)
if keyfile == "":
keyfile = None
ciphers = encryption.get('ciphers', None)
if ciphers == "":
ciphers = None

# These require passing specific constants so we use a lookup
# map for them.
addl_tls_kwargs = {}
tls_ver = encryption.get('tls_version', 'tls')
tls_version_const = TLS_VER_OPTIONS.get(tls_ver, None)
if tls_version_const is not None:
addl_tls_kwargs['tls_version'] = tls_version_const
cert_reqs = encryption.get('cert_reqs', None)
cert_reqs = CERT_REQ_OPTIONS.get(cert_reqs, None)
if cert_reqs is not None:
addl_tls_kwargs['cert_reqs'] = cert_reqs

# Finally, try the connection
try:
client.tls_set(ca_certs=ca_cert,
certfile=certfile,
keyfile=keyfile,
ciphers=ciphers, **addl_tls_kwargs)
except FileNotFoundError as e:
print("Cannot locate a SSL/TLS file = %s.", e)

except ssl.SSLError as e:
print("SSL/TLS Config error = %s.", e)

# Connect to the broker.
client.connect(config["mqtt"]["broker"], config["mqtt"]["port"])

Expand Down
2 changes: 1 addition & 1 deletion insteon_mqtt/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
variable throughout the code without causing a cyclic import
"""

__version__ = "1.0.1"
__version__ = "1.0.2"

#===========================================================================
15 changes: 11 additions & 4 deletions insteon_mqtt/data/config-base.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,8 @@ mqtt:
"stat_t": "{{heartbeat_topic}}",
"device_class": "timestamp",
"device": {{device_info_template}},
"force_update": true
"force_update": true,
"val_tpl": "{%- raw -%}{{as_datetime(value|float|timestamp_local).isoformat()|string}}{%- endraw -%}"
}
motion:
Expand Down Expand Up @@ -734,7 +735,8 @@ mqtt:
"stat_t": "{{heartbeat_topic}}",
"device_class": "timestamp",
"device": {{device_info_template}},
"force_update": true
"force_update": true,
"val_tpl": "{%- raw -%}{{as_datetime(value|float|timestamp_local).isoformat()|string}}{%- endraw -%}"
}
- component: 'sensor'
config: |-
Expand Down Expand Up @@ -765,7 +767,9 @@ mqtt:
# battery operated devices.

# Output wet/dry change topic and payload. This message is sent
# whenever the device changes state to wet or dry.
# whenever the device changes state to wet or dry. This replaces the
# state_topic that would otherwise be inheritted from battery_sensor.
# If this is not specified, then state_topic will be used instead.
# Available variables for templating are:
# address = 'aa.bb.cc'
# timestamp = the current timestamp
Expand Down Expand Up @@ -804,7 +808,8 @@ mqtt:
"name": "{{name_user_case}} heartbeat",
"stat_t": "{{heartbeat_topic}}",
"device_class": "timestamp",
"device": {{device_info_template}}
"device": {{device_info_template}},
"val_tpl": "{%- raw -%}{{as_datetime(value|float|timestamp_local).isoformat()|string}}{%- endraw -%}"
}
remote:
Expand Down Expand Up @@ -1243,6 +1248,8 @@ mqtt:
"name": "{{name_user_case}} fan",
"device": {{device_info_template}},
"cmd_t": "{{fan_on_off_topic}}",
"stat_t": "{{fan_state_topic}}",
"stat_val_tpl": "{% raw %}{{value_json.state}}{% endraw %}",
"pct_cmd_t": "{{fan_speed_set_topic}}",
"pct_cmd_tpl": "{% raw %}{% if value < 10 %}off{% elif value < 40 %}low{% elif value < 75 %}medium{% else %}high{% endif %}{% endraw %}",
"pct_stat_t": "{{fan_speed_topic}}",
Expand Down
6 changes: 3 additions & 3 deletions insteon_mqtt/device/KeypadLinc.py
Original file line number Diff line number Diff line change
Expand Up @@ -967,15 +967,15 @@ def _cache_state(self, group, is_on, level, reason):
level (int): The new device level in the range [0,255]. 0 is off.
reason (str): Reason string to pass around.
"""
if is_on is not None:
self._is_on = is_on
group = 0x01 if group is None else group
if group == self._load_group:
self._level = level
if is_on is not None:
self._is_on = is_on

# Update the LED bits in the correct slot.
if group < 9:
self._led_bits = util.bit_set(self._led_bits, group - 1,
1 if level else 0)
1 if is_on else 0)

#-----------------------------------------------------------------------
13 changes: 6 additions & 7 deletions insteon_mqtt/mqtt/Leak.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def __init__(self, mqtt, device):
device (device.Leak): The Insteon object to link to.
"""
super().__init__(mqtt, device,
state_topic='insteon/{{address}}/wet',
state_topic='insteon/{{address}}/state',
state_payload='{{is_wet_str.lower()}}')

# This defines the default discovery_class for these devices
Expand All @@ -46,7 +46,11 @@ def load_config(self, config, qos=None):
if not data:
return

# Load the various topics
# The state_topic uses a different name on the leak sensor
if 'state_topic' in self.rendered_topic_map:
self.rendered_topic_map.pop('state_topic')

# Load the special state topic
self.load_state_data(data, qos, topic='wet_dry_topic',
payload='wet_dry_payload')

Expand All @@ -56,11 +60,6 @@ def load_config(self, config, qos=None):
self.msg_heartbeat.load_config(data, 'heartbeat_topic',
'heartbeat_payload', qos)

# Add our unique topics to the discovery topic map
# The state_topic uses a different name on the leak sensor
if 'state_topic' in self.rendered_topic_map:
rendered_topic = self.rendered_topic_map.pop('state_topic')
self.rendered_topic_map['wet_dry_topic'] = rendered_topic

#-----------------------------------------------------------------------
def state_template_data(self, **kwargs):
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

setuptools.setup(
name = 'insteon-mqtt',
version = '1.0.1',
version = '1.0.2',
description = "Insteon <-> MQTT bridge server",
long_description = readme,
author = "Ted Drain",
Expand Down
6 changes: 6 additions & 0 deletions tests/device/test_KeypadLincDev.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ def on_done(success, *args):
(0x06,Msg.CmdType.ON, 0x00,{"level":255,"mode":IM.on_off.Mode.NORMAL, "is_on": True, "reason":'device', "button":6}),
(0x07,Msg.CmdType.ON, 0x00,{"level":255,"mode":IM.on_off.Mode.NORMAL, "is_on": True, "reason":'device', "button":7}),
(0x08,Msg.CmdType.ON, 0x00,{"level":255,"mode":IM.on_off.Mode.NORMAL, "is_on": True, "reason":'device', "button":8}),
(0x08,Msg.CmdType.OFF, 0x00,{"level":0,"mode":IM.on_off.Mode.NORMAL, "is_on": False, "reason":'device', "button":8}),
])
def test_handle_on_off(self, test_device, group_num, cmd1, cmd2, expected):
test_device._load_group = 1
Expand All @@ -336,6 +337,11 @@ def test_handle_on_off(self, test_device, group_num, cmd1, cmd2, expected):
mocked.assert_called_once_with(test_device, **expected)
else:
mocked.assert_not_called()
if group_num > 1:
if cmd1 == Msg.CmdType.ON:
assert test_device._led_bits == 2 ** (group_num -1)
else:
assert test_device._led_bits == 0

def test_get_flags(self, test_device):
# This should hijack get flags and should insert a call to
Expand Down
23 changes: 16 additions & 7 deletions tests/mqtt/test_Leak.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ def test_mqtt(self, setup):
dev.signal_state.emit(dev, button=2, is_on=False)
assert len(link.client.pub) == 2
assert link.client.pub[0] == dict(
topic='%s/wet' % topic, payload='on', qos=0, retain=True)
topic='%s/state' % topic, payload='on', qos=0, retain=True)
assert link.client.pub[1] == dict(
topic='%s/wet' % topic, payload='off', qos=0, retain=True)
topic='%s/state' % topic, payload='off', qos=0, retain=True)

link.client.clear()

Expand All @@ -115,11 +115,20 @@ def test_mqtt(self, setup):

#-----------------------------------------------------------------------
def test_discovery(self, setup):
mdev, dev, link = setup.getAll(['mdev', 'dev', 'link'])
topic = "insteon/%s" % setup.addr.hex
mdev = setup.get('mdev')

# Test leak defined but battery not
mdev.load_config({"leak": {'wet_dry_topic': 'insteon/{{address}}/wet'}})
assert mdev.default_discovery_cls == "leak"
assert mdev.rendered_topic_map == {
'wet_dry_topic': 'insteon/01.02.03/wet'
}
assert len(mdev.extra_topic_nums) == 0

mdev.load_config({"leak": {"junk": "junk"},
"battery_sensor" : {"junk": "junk"}})
# Test both defined
mdev.load_config({"leak": {'wet_dry_topic': 'insteon/{{address}}/wet'},
"battery_sensor" :
{'state_topic': 'insteon/{{address}}/state'}})
assert mdev.default_discovery_cls == "leak"
assert mdev.rendered_topic_map == {
'wet_dry_topic': 'insteon/01.02.03/wet',
Expand All @@ -143,7 +152,7 @@ def test_refresh_data(self, setup):
dev.signal_state.emit(dev, button=2, level=0x11, reason='refresh')
assert len(link.client.pub) == 1
assert link.client.pub[0] == dict(
topic='%s/wet' % topic, payload='on', qos=0, retain=True)
topic='%s/state' % topic, payload='on', qos=0, retain=True)

link.client.clear()

Expand Down

0 comments on commit 2ab12cd

Please sign in to comment.