|
@@ -0,0 +1,130 @@
|
|
|
|
|
+from unittest.mock import AsyncMock
|
|
|
|
|
+
|
|
|
|
|
+import pytest
|
|
|
|
|
+
|
|
|
|
|
+from switchbot import (
|
|
|
|
|
+ SwitchBotAdvertisement,
|
|
|
|
|
+ SwitchbotModel,
|
|
|
|
|
+ SwitchbotOutdoorPlug,
|
|
|
|
|
+ parse_advertisement_data,
|
|
|
|
|
+)
|
|
|
|
|
+from switchbot.adv_parsers.plug import process_outdoor_plug
|
|
|
|
|
+from switchbot.devices.device import API_MODEL_TO_ENUM
|
|
|
|
|
+
|
|
|
|
|
+from .test_adv_parser import generate_advertisement_data, generate_ble_device
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@pytest.mark.parametrize("network", [0, 0x15, 0x75])
|
|
|
|
|
+@pytest.mark.parametrize(("status", "is_on"), [(0x01, False), (0x87, True)])
|
|
|
|
|
+@pytest.mark.parametrize("encrypted", [False, True])
|
|
|
|
|
+def test_outdoor_plug_eu_advertisement(network, status, is_on, encrypted):
|
|
|
|
|
+ service = bytes([0x80 if encrypted else 0, 0, 0, 0, 0x12, 0x18, 0xD0, network])
|
|
|
|
|
+ manufacturer = (
|
|
|
|
|
+ bytes.fromhex("010203040506")
|
|
|
|
|
+ + bytes([99, status, 0, 0])
|
|
|
|
|
+ + bytes.fromhex("04e6007b0000")
|
|
|
|
|
+ )
|
|
|
|
|
+ adv = parse_advertisement_data(
|
|
|
|
|
+ generate_ble_device("01:02:03:04:05:06", "Outdoor Plug (EU)"),
|
|
|
|
|
+ generate_advertisement_data(
|
|
|
|
|
+ manufacturer_data={2409: manufacturer},
|
|
|
|
|
+ service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": service},
|
|
|
|
|
+ ),
|
|
|
|
|
+ )
|
|
|
|
|
+ assert adv is not None
|
|
|
|
|
+ assert adv.data["modelName"] == SwitchbotModel.OUTDOOR_PLUG_EU
|
|
|
|
|
+ assert adv.data["modelFriendlyName"] == "Outdoor Plug (EU)"
|
|
|
|
|
+ assert adv.data["isEncrypted"] is encrypted
|
|
|
|
|
+ assert adv.data["data"] == {
|
|
|
|
|
+ "switchMode": True,
|
|
|
|
|
+ "sequence_number": 99,
|
|
|
|
|
+ "isOn": is_on,
|
|
|
|
|
+ "power": 1254.0,
|
|
|
|
|
+ "power_production": 123.0,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def create_device():
|
|
|
|
|
+ return SwitchbotOutdoorPlug(
|
|
|
|
|
+ generate_ble_device("01:02:03:04:05:06", "Outdoor Plug"),
|
|
|
|
|
+ "ff",
|
|
|
|
|
+ "ffffffffffffffffffffffffffffffff",
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
|
+@pytest.mark.parametrize("is_on", [False, True])
|
|
|
|
|
+async def test_bidirectional_metering(is_on):
|
|
|
|
|
+ device = create_device()
|
|
|
|
|
+ device.get_current_time_and_start_time = lambda: ("00000010", "00000000")
|
|
|
|
|
+ common = bytes([1, 99, 0x81 if is_on else 1]) + bytes(13) + bytes([3])
|
|
|
|
|
+ consumed = bytes.fromhex("0100ea60003a98003c08fc03e808fc")
|
|
|
|
|
+ produced = bytes.fromhex("0100bb80007530001e08fc07d011f8")
|
|
|
|
|
+ device._get_basic_info = AsyncMock(side_effect=[common, consumed, produced])
|
|
|
|
|
+ info = await device.get_basic_info()
|
|
|
|
|
+ assert info["isOn"] is is_on
|
|
|
|
|
+ assert info["energy"] == 1.0
|
|
|
|
|
+ assert info["energy_production"] == 0.8
|
|
|
|
|
+ assert info["power"] == (230.0 if is_on else 0)
|
|
|
|
|
+ assert info["power_production"] == (460.0 if is_on else 0)
|
|
|
|
|
+ assert info["current"] == (1.0 if is_on else 0)
|
|
|
|
|
+ assert info["current_production"] == (2.0 if is_on else 0)
|
|
|
|
|
+ assert info["voltage"] == (230 if is_on else 0)
|
|
|
|
|
+ assert device._get_basic_info.call_args_list[1].args == (
|
|
|
|
|
+ "570f7106000000001000000000",
|
|
|
|
|
+ )
|
|
|
|
|
+ assert device._get_basic_info.call_args_list[2].args == (
|
|
|
|
|
+ "570f7106010000001000000000",
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
|
+@pytest.mark.parametrize("reply", [None, b"", b"\x01", bytes(14)])
|
|
|
|
|
+async def test_short_production_response(reply):
|
|
|
|
|
+ device = create_device()
|
|
|
|
|
+ common = bytes([1, 99, 0x81]) + bytes(13) + bytes([3])
|
|
|
|
|
+ device._get_basic_info = AsyncMock(side_effect=[common, bytes(15), reply])
|
|
|
|
|
+ assert await device.get_basic_info() is None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@pytest.mark.parametrize("manufacturer", [None, b"", bytes(13)])
|
|
|
|
|
+def test_short_advertisement(manufacturer):
|
|
|
|
|
+
|
|
|
|
|
+ assert process_outdoor_plug(None, manufacturer) == {}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
|
+async def test_advertisement_preserves_energy_and_refreshes_power():
|
|
|
|
|
+ device = create_device()
|
|
|
|
|
+ device.update = AsyncMock()
|
|
|
|
|
+
|
|
|
|
|
+ def advertisement(sequence, power, production):
|
|
|
|
|
+ return SwitchBotAdvertisement(
|
|
|
|
|
+ address="01:02:03:04:05:06",
|
|
|
|
|
+ data={
|
|
|
|
|
+ "data": {
|
|
|
|
|
+ "sequence_number": sequence,
|
|
|
|
|
+ "isOn": True,
|
|
|
|
|
+ "power": power,
|
|
|
|
|
+ "power_production": production,
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ device=device._device,
|
|
|
|
|
+ rssi=-40,
|
|
|
|
|
+ active=True,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ device.update_from_advertisement(advertisement(1, 20, 30))
|
|
|
|
|
+ device.data["data"].update(
|
|
|
|
|
+ {"energy": 1.2, "energy_production": 3.4, "current_production": 0.5}
|
|
|
|
|
+ )
|
|
|
|
|
+ device.update_from_advertisement(advertisement(2, 40, 50))
|
|
|
|
|
+ assert device.parsed_data["energy"] == 1.2
|
|
|
|
|
+ assert device.parsed_data["energy_production"] == 3.4
|
|
|
|
|
+ assert device.parsed_data["current_production"] == 0.5
|
|
|
|
|
+ assert device.parsed_data["power"] == 40
|
|
|
|
|
+ assert device.parsed_data["power_production"] == 50
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_cloud_model_mapping():
|
|
|
|
|
+ assert API_MODEL_TO_ENUM["W1186000"] is SwitchbotModel.OUTDOOR_PLUG_EU
|