Browse Source

publish new state to homeassistant/switch/switchbot/MAC_ADDRESS/state on success

Fabian Peter Hammerle 4 years ago
parent
commit
ce8cf8c8bf
5 changed files with 112 additions and 18 deletions
  1. 2 0
      CHANGELOG.md
  2. 1 0
      README.md
  3. 60 14
      switchbot_mqtt/__init__.py
  4. 33 2
      tests/test_mqtt.py
  5. 16 2
      tests/test_switchbot.py

+ 2 - 0
CHANGELOG.md

@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
+### Added
+- Publish new state to `homeassistant/switch/switchbot/MAC_ADDRESS/state` on success
 
 ## [0.2.0] - 2020-05-08
 ### Added

+ 1 - 0
README.md

@@ -64,6 +64,7 @@ switch:
 - platform: mqtt
   name: some_name
   command_topic: homeassistant/switch/switchbot/aa:bb:cc:dd:ee:ff/set
+  state_topic: homeassistant/switch/switchbot/aa:bb:cc:dd:ee:ff/state
   # http://materialdesignicons.com/
   icon: mdi:light-switch
 ```

+ 60 - 14
switchbot_mqtt/__init__.py

@@ -27,25 +27,35 @@ import switchbot
 
 _LOGGER = logging.getLogger(__name__)
 
+
+class _SwitchbotAction(enum.Enum):
+    ON = 1
+    OFF = 2
+
+
+class _SwitchbotState(enum.Enum):
+    ON = 1
+    OFF = 2
+
+
+# https://www.home-assistant.io/docs/mqtt/discovery/#switches
+_MQTT_TOPIC_PREFIX_LEVELS = ["homeassistant", "switch", "switchbot"]
 _MQTT_TOPIC_MAC_ADDRESS_PLACEHOLDER = "{mac_address}"
-_MQTT_SET_TOPIC_PATTERN = [
-    "homeassistant",
-    "switch",
-    "switchbot",
+_MQTT_SET_TOPIC_LEVELS = _MQTT_TOPIC_PREFIX_LEVELS + [
     _MQTT_TOPIC_MAC_ADDRESS_PLACEHOLDER,
     "set",
 ]
-_MQTT_SET_TOPIC = "/".join(_MQTT_SET_TOPIC_PATTERN).replace(
+_MQTT_SET_TOPIC = "/".join(_MQTT_SET_TOPIC_LEVELS).replace(
     _MQTT_TOPIC_MAC_ADDRESS_PLACEHOLDER, "+"
 )
+_MQTT_STATE_TOPIC = "/".join(
+    _MQTT_TOPIC_PREFIX_LEVELS + [_MQTT_TOPIC_MAC_ADDRESS_PLACEHOLDER, "state"]
+)
+# https://www.home-assistant.io/integrations/switch.mqtt/#state_off
+_MQTT_STATE_PAYLOAD_MAPPING = {_SwitchbotState.ON: b"ON", _SwitchbotState.OFF: b"OFF"}
 _MAC_ADDRESS_REGEX = re.compile(r"^[0-9a-f]{2}(:[0-9a-f]{2}){5}$")
 
 
-class _SwitchbotAction(enum.Enum):
-    ON = 1
-    OFF = 2
-
-
 def _mac_address_valid(mac_address: str) -> bool:
     return _MAC_ADDRESS_REGEX.match(mac_address.lower()) is not None
 
@@ -65,19 +75,51 @@ def _mqtt_on_connect(
     mqtt_client.subscribe(_MQTT_SET_TOPIC)
 
 
-def _send_command(switchbot_mac_address: str, action: _SwitchbotAction) -> None:
+def _report_state(
+    mqtt_client: paho.mqtt.client.Client,
+    switchbot_mac_address: str,
+    switchbot_state: _SwitchbotState,
+) -> None:
+    # https://pypi.org/project/paho-mqtt/#publishing
+    message_info: paho.mqtt.client.MQTTMessageInfo = mqtt_client.publish(
+        topic=_MQTT_STATE_TOPIC.replace(
+            _MQTT_TOPIC_MAC_ADDRESS_PLACEHOLDER, switchbot_mac_address
+        ),
+        payload=_MQTT_STATE_PAYLOAD_MAPPING[switchbot_state],
+        retain=True,
+    )
+    print("TODO", message_info.rc)
+    message_info.wait_for_publish()
+    print("TODO", message_info.rc, message_info.is_published)
+
+
+def _send_command(
+    mqtt_client: paho.mqtt.client.Client,
+    switchbot_mac_address: str,
+    action: _SwitchbotAction,
+) -> None:
     switchbot_device = switchbot.Switchbot(mac=switchbot_mac_address)
     if action == _SwitchbotAction.ON:
         if not switchbot_device.turn_on():
             _LOGGER.error("failed to turn on switchbot %s", switchbot_mac_address)
         else:
             _LOGGER.info("switchbot %s turned on", switchbot_mac_address)
+            _report_state(
+                mqtt_client=mqtt_client,
+                switchbot_mac_address=switchbot_mac_address,
+                switchbot_state=_SwitchbotState.ON,
+            )
     else:
         assert action == _SwitchbotAction.OFF, action
         if not switchbot_device.turn_off():
             _LOGGER.error("failed to turn off switchbot %s", switchbot_mac_address)
         else:
             _LOGGER.info("switchbot %s turned off", switchbot_mac_address)
+            _report_state(
+                mqtt_client=mqtt_client,
+                switchbot_mac_address=switchbot_mac_address,
+                switchbot_state=_SwitchbotState.OFF,
+            )
 
 
 def _mqtt_on_message(
@@ -92,11 +134,11 @@ def _mqtt_on_message(
         _LOGGER.info("ignoring retained message")
         return
     topic_split = message.topic.split("/")
-    if len(topic_split) != len(_MQTT_SET_TOPIC_PATTERN):
+    if len(topic_split) != len(_MQTT_SET_TOPIC_LEVELS):
         _LOGGER.warning("unexpected topic %s", message.topic)
         return
     switchbot_mac_address = None
-    for given_part, expected_part in zip(topic_split, _MQTT_SET_TOPIC_PATTERN):
+    for given_part, expected_part in zip(topic_split, _MQTT_SET_TOPIC_LEVELS):
         if expected_part == _MQTT_TOPIC_MAC_ADDRESS_PLACEHOLDER:
             switchbot_mac_address = given_part
         elif expected_part != given_part:
@@ -114,7 +156,11 @@ def _mqtt_on_message(
     else:
         _LOGGER.warning("unexpected payload %r", message.payload)
         return
-    _send_command(switchbot_mac_address=switchbot_mac_address, action=action)
+    _send_command(
+        mqtt_client=mqtt_client,
+        switchbot_mac_address=switchbot_mac_address,
+        action=action,
+    )
 
 
 def _run(

+ 33 - 2
tests/test_mqtt.py

@@ -111,9 +111,11 @@ def test__mqtt_on_message(
     message = MQTTMessage(topic=topic)
     message.payload = payload
     with unittest.mock.patch("switchbot_mqtt._send_command") as send_command_mock:
-        switchbot_mqtt._mqtt_on_message(None, None, message)
+        switchbot_mqtt._mqtt_on_message("client_dummy", None, message)
     send_command_mock.assert_called_once_with(
-        switchbot_mac_address=expected_mac_address, action=expected_action
+        mqtt_client="client_dummy",
+        switchbot_mac_address=expected_mac_address,
+        action=expected_action,
     )
 
 
@@ -150,3 +152,32 @@ def test__mqtt_on_message_ignored_retained(
     with unittest.mock.patch("switchbot_mqtt._send_command") as send_command_mock:
         switchbot_mqtt._mqtt_on_message(None, None, message)
     assert not send_command_mock.called
+
+
+@pytest.mark.parametrize(
+    ("switchbot_mac_address", "expected_topic"),
+    # https://www.home-assistant.io/docs/mqtt/discovery/#switches
+    [("aa:bb:cc:dd:ee:ff", "homeassistant/switch/switchbot/aa:bb:cc:dd:ee:ff/state")],
+)
+@pytest.mark.parametrize(
+    ("state", "expected_payload"),
+    [
+        (switchbot_mqtt._SwitchbotState.ON, b"ON"),
+        (switchbot_mqtt._SwitchbotState.OFF, b"OFF"),
+    ],
+)
+def test__report_state(
+    state: switchbot_mqtt._SwitchbotState,
+    switchbot_mac_address: str,
+    expected_topic: str,
+    expected_payload: bytes,
+):
+    mqtt_client_mock = unittest.mock.MagicMock()
+    switchbot_mqtt._report_state(
+        mqtt_client=mqtt_client_mock,
+        switchbot_mac_address=switchbot_mac_address,
+        switchbot_state=state,
+    )
+    mqtt_client_mock.publish.assert_called_once_with(
+        topic=expected_topic, payload=expected_payload, retain=True,
+    )

+ 16 - 2
tests/test_switchbot.py

@@ -18,8 +18,13 @@ def test__send_command(caplog, mac_address, action, command_successful):
         switchbot_device_mock().turn_on.return_value = command_successful
         switchbot_device_mock().turn_off.return_value = command_successful
         switchbot_device_mock.reset_mock()
-        with caplog.at_level(logging.INFO):
-            switchbot_mqtt._send_command(mac_address, action)
+        with unittest.mock.patch("switchbot_mqtt._report_state") as report_mock:
+            with caplog.at_level(logging.INFO):
+                switchbot_mqtt._send_command(
+                    mqtt_client="dummy",
+                    switchbot_mac_address=mac_address,
+                    action=action,
+                )
     switchbot_device_mock.assert_called_once_with(mac=mac_address)
     assert len(caplog.records) == 1
     logger, log_level, log_message = caplog.record_tuples[0]
@@ -34,7 +39,16 @@ def test__send_command(caplog, mac_address, action, command_successful):
         switchbot_device_mock().turn_on.assert_called_once_with()
         assert not switchbot_device_mock().turn_off.called
         assert "on" in log_message
+        expected_state = switchbot_mqtt._SwitchbotState.ON
     else:
         switchbot_device_mock().turn_off.assert_called_once_with()
         assert not switchbot_device_mock().turn_on.called
         assert "off" in log_message
+        expected_state = switchbot_mqtt._SwitchbotState.OFF
+    assert report_mock.called == command_successful
+    if command_successful:
+        report_mock.assert_called_once_with(
+            mqtt_client="dummy",
+            switchbot_mac_address=mac_address,
+            switchbot_state=expected_state,
+        )