diff --git a/README.md b/README.md index 163b1ad..f892d8e 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,7 @@ When Home Assistant publishes its birth message (`online` on `homeassistant/stat - Event - Device Trigger - Tag Scanner +- Notify @@ -427,6 +428,15 @@ scanner = uhome.TagScanner(device, 'RFID Reader') scanner.scan('E9F35959') ``` +### Notify + +Use `Notify` when Home Assistant should send notification messages to the device over MQTT. The device subscribes to the generated command topic, and uhome restores that subscription after reconnects. + +``` +display = uhome.Notify(device, 'Display') +display.set_action(lambda msg: print(msg)) +``` + ## Testing Desktop tests can be run from the repository root with: diff --git a/tests/test_notify.py b/tests/test_notify.py new file mode 100644 index 0000000..de225b1 --- /dev/null +++ b/tests/test_notify.py @@ -0,0 +1,86 @@ +import os +import sys +import unittest + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(ROOT, "uhome")) + +from test_uhome import FakeClock, FakeMQTTClient + +import uhome + + +class NotifyTests(unittest.TestCase): + def setUp(self): + self.clock = FakeClock() + self._old_ticks_ms = uhome.ticks_ms + self._old_ticks_diff = uhome.ticks_diff + uhome.ticks_ms = self.clock.ticks_ms + uhome.ticks_diff = self.clock.ticks_diff + + def tearDown(self): + uhome.ticks_ms = self._old_ticks_ms + uhome.ticks_diff = self._old_ticks_diff + + def payloads_for(self, mqtt, topic): + return [payload for pub_topic, payload, retain, qos in mqtt.published if pub_topic == topic] + + def test_discovery_config_contains_notify_schema(self): + device = uhome.Device("Notify Device") + notify = uhome.Notify(device, "Display", command_template="{{ message }}", retain=True) + + self.assertEqual("Notify Device", notify.conf["dev"]["name"]) + self.assertEqual("notify_device_display", notify.conf["uniq_id"]) + self.assertEqual(device.will_topic, notify.conf["avty_t"]) + self.assertEqual(notify.topic_for("command"), notify.conf["cmd_t"]) + self.assertEqual("{{ message }}", notify.conf["command_template"]) + self.assertTrue(notify.conf["retain"]) + self.assertNotIn("stat_t", notify.conf) + + def test_set_action_subscribes_and_handles_notifications(self): + device = uhome.Device("Notify Device") + notify = uhome.Notify(device, "Display") + received = [] + self.assertTrue(notify.set_action(lambda msg: received.append(msg))) + mqtt = FakeMQTTClient() + self.assertTrue(device.connect(mqtt)) + + self.assertIn((notify.get_topic(), 0), mqtt.subscribed) + mqtt.deliver(notify.get_topic(), "hello") + self.assertTrue(device.loop()) + self.assertEqual(["hello"], received) + + def test_command_subscription_survives_reconnect(self): + device = uhome.Device("Notify Device") + notify = uhome.Notify(device, "Display") + received = [] + notify.set_action(lambda msg: received.append(msg)) + mqtt = FakeMQTTClient() + self.assertTrue(device.connect(mqtt)) + mqtt.clear_history() + + mqtt.fail_publish = True + self.assertFalse(device.publish("any/topic", "x")) + mqtt.fail_publish = False + self.clock.advance(1000) + self.assertTrue(device.loop()) + + self.assertIn((notify.get_topic(), 0), mqtt.subscribed) + self.assertTrue(self.payloads_for(mqtt, notify.discovery_topic)) + mqtt.deliver(notify.get_topic(), "after reconnect") + self.assertTrue(device.loop()) + self.assertEqual(["after reconnect"], received) + + def test_notify_has_no_cached_state_to_republish(self): + device = uhome.Device("Notify Device") + notify = uhome.Notify(device, "Display") + mqtt = FakeMQTTClient() + self.assertTrue(device.connect(mqtt)) + mqtt.clear_history() + + self.assertTrue(notify.republish()) + self.assertEqual([], mqtt.published) + + +if __name__ == "__main__": + unittest.main() diff --git a/uhome/uhome.py b/uhome/uhome.py index 2a6e2cc..1dab35e 100644 --- a/uhome/uhome.py +++ b/uhome/uhome.py @@ -1195,6 +1195,45 @@ def republish(self): return True +class Notify(Entity): + """ + MQTT notify entity that receives Home Assistant notification messages. + + Example:: + + display = uhome.Notify(device, "Display") + display.set_action(lambda msg: print(msg)) + """ + + entity_type = 'notify' + _action = None + + def make_conf(self, **kwargs): + conf = super().make_conf(**kwargs) + conf["cmd_t"] = self.topic_for("command") + return conf + + def get_topic(self): + return self.conf['cmd_t'] + + def set_action(self, action): + """ + Subscribe to notifications sent by Home Assistant's notify service. + """ + self._action = action + return self._subscribe(self.conf['cmd_t'], self._handle_action) + + def republish(self): + """ + Notify entities receive commands from Home Assistant and have no cached state. + """ + return True + + def _handle_action(self, msg): + if self._action: + self._action(msg) + + class Sensor(Entity): """ More information about MQTT Sensors: https://www.home-assistant.io/integrations/sensor.mqtt/