Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/libdeye/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ class DeyeProductConfig(TypedDict):
water_pump: bool
single_property_fog_commands: bool
requires_power_in_fog_partial_updates: bool
requires_mode_in_fog_power_on_updates: bool
omit_set_humidity_in_fog_auto_updates: bool


class DeyeProductPartialConfig(TypedDict, total=False):
Expand All @@ -57,6 +59,8 @@ class DeyeProductPartialConfig(TypedDict, total=False):
water_pump: bool
single_property_fog_commands: bool
requires_power_in_fog_partial_updates: bool
requires_mode_in_fog_power_on_updates: bool
omit_set_humidity_in_fog_auto_updates: bool


PRODUCT_FEATURE_CONFIG: dict[str, DeyeProductPartialConfig] = {
Expand Down Expand Up @@ -215,6 +219,8 @@ class DeyeProductPartialConfig(TypedDict, total=False):
"oscillating": False,
"water_pump": False,
"requires_power_in_fog_partial_updates": True,
"requires_mode_in_fog_power_on_updates": True,
"omit_set_humidity_in_fog_auto_updates": True,
},
"17ab051af38611e89d4c00163e0c1b21": { # W20A3
"mode": [
Expand Down Expand Up @@ -486,6 +492,8 @@ def get_product_feature_config(product_id: str) -> DeyeProductConfig:
"water_pump": True,
"single_property_fog_commands": False,
"requires_power_in_fog_partial_updates": False,
"requires_mode_in_fog_power_on_updates": False,
"omit_set_humidity_in_fog_auto_updates": False,
}
try:
return default | PRODUCT_FEATURE_CONFIG[product_id]
Expand Down
18 changes: 17 additions & 1 deletion src/libdeye/mqtt_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
import paho.mqtt.client as mqtt

from .cloud_api import DeyeApiResponseFogPlatformDeviceProperties, DeyeCloudApi
from .const import QUERY_DEVICE_STATE_COMMAND_CLASSIC, get_product_feature_config
from .const import (
QUERY_DEVICE_STATE_COMMAND_CLASSIC,
DeyeDeviceMode,
get_product_feature_config,
)
from .device_command import DeyeDeviceCommand
from .device_state import DeyeDeviceState

Expand Down Expand Up @@ -308,6 +312,10 @@ async def publish_command(
properties_to_publish = (
dict(properties) if properties is not None else command.to_json()
)
powering_on = properties_to_publish.get("Power") == 1
if powering_on and feature_config["requires_mode_in_fog_power_on_updates"]:
properties_to_publish["Mode"] = int(command.mode)

if (
properties is not None
and properties_to_publish
Expand All @@ -317,6 +325,14 @@ async def publish_command(
):
properties_to_publish["Power"] = 1

if (
feature_config["omit_set_humidity_in_fog_auto_updates"]
and command.mode == DeyeDeviceMode.AUTO_MODE
):
properties_to_publish.pop("SetHumidity", None)
if not properties_to_publish:
return

if feature_config["single_property_fog_commands"]:
if "Power" in properties_to_publish:
await self._cloud_api.set_fog_platform_device_properties(
Expand Down
7 changes: 7 additions & 0 deletions tests/test_const.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,15 @@ def test_get_product_feature_config() -> None:

u20a3_config = get_product_feature_config("20eae2ea268511e8829100163e0f811e")
assert u20a3_config["requires_power_in_fog_partial_updates"] is True
assert u20a3_config["requires_mode_in_fog_power_on_updates"] is False
assert u20a3_config["omit_set_humidity_in_fog_auto_updates"] is False
assert u20a3_config["single_property_fog_commands"] is False

u20air_config = get_product_feature_config("363b686a31ee11efb7203b3cd9717242")
assert u20air_config["requires_power_in_fog_partial_updates"] is False
assert u20air_config["single_property_fog_commands"] is True

v58a3_config = get_product_feature_config("2b770cba268611e89d4c00163e0c1b21")
assert v58a3_config["requires_power_in_fog_partial_updates"] is True
assert v58a3_config["requires_mode_in_fog_power_on_updates"] is True
assert v58a3_config["omit_set_humidity_in_fog_auto_updates"] is True
131 changes: 130 additions & 1 deletion tests/test_mqtt_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
DeyeApiResponseFogPlatformMqttTopics,
DeyeCloudApi,
)
from libdeye.const import QUERY_DEVICE_STATE_COMMAND_CLASSIC
from libdeye.const import QUERY_DEVICE_STATE_COMMAND_CLASSIC, DeyeDeviceMode
from libdeye.device_command import DeyeDeviceCommand
from libdeye.device_state import DeyeDeviceState
from libdeye.mqtt_client import (
Expand Down Expand Up @@ -556,6 +556,135 @@ async def test_publish_command_includes_power_for_u20a3_partial_updates(
)
assert properties == {"SetHumidity": 70}

@pytest.mark.asyncio
async def test_publish_command_reasserts_mode_for_v58a3_power_on(
self, fog_client: DeyeFogMqttClient
) -> None:
"""Test V58A3 wakes in auto mode without publishing target humidity."""
product_id = "2b770cba268611e89d4c00163e0c1b21"
device_id = "device456"
command = DeyeDeviceCommand(
power_switch=True,
mode=DeyeDeviceMode.AUTO_MODE,
target_humidity=25,
)
properties = {"Power": 1, "SetHumidity": 25}

await fog_client.publish_command(
product_id,
device_id,
command,
properties=properties,
)

cast(
MagicMock, fog_client._cloud_api
).set_fog_platform_device_properties.assert_awaited_once_with(
device_id,
{"Power": 1, "Mode": int(DeyeDeviceMode.AUTO_MODE)},
)
assert properties == {"Power": 1, "SetHumidity": 25}

@pytest.mark.asyncio
async def test_publish_command_preserves_v58a3_power_for_auto_partial_updates(
self, fog_client: DeyeFogMqttClient
) -> None:
"""Test V58A3 auto updates preserve power before omitting humidity."""
product_id = "2b770cba268611e89d4c00163e0c1b21"
device_id = "device456"
command = DeyeDeviceCommand(
power_switch=True,
mode=DeyeDeviceMode.AUTO_MODE,
target_humidity=25,
)

await fog_client.publish_command(
product_id,
device_id,
command,
properties={"SetHumidity": 25},
)

cast(
MagicMock, fog_client._cloud_api
).set_fog_platform_device_properties.assert_awaited_once_with(
device_id, {"Power": 1}
)

@pytest.mark.asyncio
async def test_publish_command_omits_v58a3_auto_humidity_while_powered_off(
self, fog_client: DeyeFogMqttClient
) -> None:
"""Test V58A3 does not publish an empty auto humidity update."""
product_id = "2b770cba268611e89d4c00163e0c1b21"
command = DeyeDeviceCommand(
power_switch=False,
mode=DeyeDeviceMode.AUTO_MODE,
target_humidity=25,
)

await fog_client.publish_command(
product_id,
"device456",
command,
properties={"SetHumidity": 25},
)

cast(
MagicMock, fog_client._cloud_api
).set_fog_platform_device_properties.assert_not_awaited()

@pytest.mark.asyncio
async def test_publish_command_preserves_v58a3_power_for_manual_updates(
self, fog_client: DeyeFogMqttClient
) -> None:
"""Test V58A3 manual updates include the current powered-on state."""
product_id = "2b770cba268611e89d4c00163e0c1b21"
command = DeyeDeviceCommand(
power_switch=True,
mode=DeyeDeviceMode.MANUAL_MODE,
target_humidity=70,
)

await fog_client.publish_command(
product_id,
"device456",
command,
properties={"SetHumidity": 70},
)

cast(
MagicMock, fog_client._cloud_api
).set_fog_platform_device_properties.assert_awaited_once_with(
"device456", {"SetHumidity": 70, "Power": 1}
)

@pytest.mark.asyncio
async def test_publish_command_does_not_apply_v58a3_quirks_to_other_products(
self, fog_client: DeyeFogMqttClient
) -> None:
"""Test generic Fog products keep their power-on auto properties unchanged."""
product_id = "product123"
device_id = "device456"
command = DeyeDeviceCommand(
power_switch=True,
mode=DeyeDeviceMode.AUTO_MODE,
target_humidity=25,
)

await fog_client.publish_command(
product_id,
device_id,
command,
properties={"Power": 1, "SetHumidity": 25},
)

cast(
MagicMock, fog_client._cloud_api
).set_fog_platform_device_properties.assert_awaited_once_with(
device_id, {"Power": 1, "SetHumidity": 25}
)

@pytest.mark.asyncio
async def test_publish_command_splits_properties_for_u20air(
self, fog_client: DeyeFogMqttClient
Expand Down
Loading