Skip to content

Commit

Permalink
Merge 087dc68 into 5253c31
Browse files Browse the repository at this point in the history
  • Loading branch information
fphammerle committed May 8, 2020
2 parents 5253c31 + 087dc68 commit 871780d
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 19 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -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
```
Expand Down
74 changes: 60 additions & 14 deletions switchbot_mqtt/__init__.py
Expand Up @@ -27,25 +27,35 @@

_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

Expand All @@ -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
topic = _MQTT_STATE_TOPIC.replace(
_MQTT_TOPIC_MAC_ADDRESS_PLACEHOLDER, switchbot_mac_address
)
payload = _MQTT_STATE_PAYLOAD_MAPPING[switchbot_state]
_LOGGER.debug("publishing topic=%s payload=%r", topic, payload)
message_info: paho.mqtt.client.MQTTMessageInfo = mqtt_client.publish(
topic=topic, payload=payload, retain=True,
)
if message_info.rc != paho.mqtt.client.MQTT_ERR_SUCCESS:
_LOGGER.error("failed to publish state (rc=%d)", message_info.rc)


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(
Expand All @@ -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:
Expand All @@ -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(
Expand Down
55 changes: 52 additions & 3 deletions tests/test_mqtt.py
@@ -1,7 +1,8 @@
import logging
import unittest.mock

import pytest
from paho.mqtt.client import MQTTMessage
from paho.mqtt.client import MQTT_ERR_QUEUE_SIZE, MQTT_ERR_SUCCESS, MQTTMessage

import switchbot_mqtt

Expand Down Expand Up @@ -111,9 +112,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,
)


Expand Down Expand Up @@ -150,3 +153,49 @@ 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"),
],
)
@pytest.mark.parametrize(
"return_code", [MQTT_ERR_SUCCESS, MQTT_ERR_QUEUE_SIZE],
)
def test__report_state(
caplog,
state: switchbot_mqtt._SwitchbotState,
switchbot_mac_address: str,
expected_topic: str,
expected_payload: bytes,
return_code: int,
):
# pylint: disable=too-many-arguments
mqtt_client_mock = unittest.mock.MagicMock()
mqtt_client_mock.publish.return_value.rc = return_code
with caplog.at_level(logging.WARNING):
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,
)
if return_code == MQTT_ERR_SUCCESS:
assert len(caplog.records) == 0
else:
assert len(caplog.records) == 1
assert caplog.record_tuples[0] == (
"switchbot_mqtt",
logging.ERROR,
"failed to publish state (rc={})".format(return_code),
)
18 changes: 16 additions & 2 deletions tests/test_switchbot.py
Expand Up @@ -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]
Expand All @@ -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,
)

0 comments on commit 871780d

Please sign in to comment.