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
11 changes: 9 additions & 2 deletions src/libdeye/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,15 @@ async def set_device_state(
if child_lock is not None:
command.child_lock_switch = child_lock

# Send the command
await mqtt_client.publish_command(device_info["product_id"], device_id, command)
properties = (
command.to_json_diff(state) if platform == DeyeIotPlatform.Fog else None
)
await mqtt_client.publish_command(
device_info["product_id"],
device_id,
command,
properties=properties,
)

print(f"Command sent to device {device_info['device_name']} ({device_id})")

Expand Down
4 changes: 4 additions & 0 deletions src/libdeye/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class DeyeProductConfig(TypedDict):
anion: bool
oscillating: bool
water_pump: bool
single_property_fog_commands: bool


class DeyeProductPartialConfig(TypedDict, total=False):
Expand All @@ -53,6 +54,7 @@ class DeyeProductPartialConfig(TypedDict, total=False):
anion: bool
oscillating: bool
water_pump: bool
single_property_fog_commands: bool


PRODUCT_FEATURE_CONFIG: dict[str, DeyeProductPartialConfig] = {
Expand Down Expand Up @@ -204,6 +206,7 @@ class DeyeProductPartialConfig(TypedDict, total=False):
],
"oscillating": False,
"water_pump": False,
"single_property_fog_commands": True,
},
"2b770cba268611e89d4c00163e0c1b21": { # V58A3
"oscillating": False,
Expand Down Expand Up @@ -477,6 +480,7 @@ def get_product_feature_config(product_id: str) -> DeyeProductConfig:
"anion": True,
"oscillating": True,
"water_pump": True,
"single_property_fog_commands": False,
}
try:
return default | PRODUCT_FEATURE_CONFIG[product_id]
Expand Down
20 changes: 18 additions & 2 deletions src/libdeye/mqtt_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import paho.mqtt.client as mqtt

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

Expand Down Expand Up @@ -304,8 +304,24 @@ async def publish_command(
For Fog platform, commands are not published via MQTT.
Instead, use the cloud API to send commands.
"""
properties_to_publish = (
properties if properties is not None else command.to_json()
)
if get_product_feature_config(product_id)["single_property_fog_commands"]:
if "Power" in properties_to_publish:
await self._cloud_api.set_fog_platform_device_properties(
device_id, {"Power": properties_to_publish["Power"]}
)
for name, value in properties_to_publish.items():
if name == "Power":
continue
await self._cloud_api.set_fog_platform_device_properties(
device_id, {name: value}
)
return

await self._cloud_api.set_fog_platform_device_properties(
device_id, properties if properties is not None else command.to_json()
device_id, properties_to_publish
)

async def query_device_state(
Expand Down
43 changes: 42 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from libdeye.device_state import DeyeDeviceState
from libdeye.mqtt_client import (
DeyeClassicMqttClient,
DeyeFogMqttClient,
)


Expand Down Expand Up @@ -320,11 +321,51 @@ async def test_set_device_state() -> None:
assert mock_command.child_lock_switch is False

mock_mqtt_client.publish_command.assert_called_once_with(
"test_product_id", "test_device_id", mock_command
"test_product_id",
"test_device_id",
mock_command,
properties=None,
)
mock_mqtt_client.disconnect.assert_called_once()


@pytest.mark.asyncio
async def test_set_fog_device_state_publishes_only_changed_properties() -> None:
"""Test Fog commands publish only properties changed by the CLI invocation."""
mock_api = AsyncMock(spec=DeyeCloudApi)
mock_api.get_device_list.return_value = [
{
"device_name": "Test Device",
"device_id": "test_device_id",
"product_id": "test_product_id",
"platform": DeyeIotPlatform.Fog.value,
}
]

mock_mqtt_client = AsyncMock(spec=DeyeFogMqttClient)
mock_state = MagicMock(spec=DeyeDeviceState)
mock_command = MagicMock()
mock_state.to_command.return_value = mock_command
mock_command.to_json_diff.return_value = {"SetHumidity": 30}
mock_mqtt_client.query_device_state.return_value = mock_state

with patch("libdeye.cli.DeyeFogMqttClient", return_value=mock_mqtt_client):
await set_device_state(
mock_api,
"test_device_id",
target_humidity=30,
)

mock_command.to_json_diff.assert_called_once_with(mock_state)
mock_mqtt_client.publish_command.assert_awaited_once_with(
"test_product_id",
"test_device_id",
mock_command,
properties={"SetHumidity": 30},
)
mock_mqtt_client.disconnect.assert_called_once()


@pytest.mark.asyncio
async def test_monitor_device() -> None:
"""Test monitoring device."""
Expand Down
27 changes: 26 additions & 1 deletion tests/test_mqtt_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import asyncio
import json
from typing import Any, Callable, cast
from unittest.mock import AsyncMock, MagicMock, patch
from unittest.mock import AsyncMock, MagicMock, call, patch

import paho.mqtt.client as mqtt
import pytest
Expand Down Expand Up @@ -530,6 +530,31 @@ async def test_publish_command_with_properties(
MagicMock, fog_client._cloud_api
).set_fog_platform_device_properties.call_args[0] == (device_id, {"Power": 1})

@pytest.mark.asyncio
async def test_publish_command_splits_properties_for_u20air(
self, fog_client: DeyeFogMqttClient
) -> None:
"""Test U20Air property updates are sent one per API request."""
product_id = "363b686a31ee11efb7203b3cd9717242"
device_id = "device456"
command = DeyeDeviceCommand(power_switch=True, target_humidity=70)

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

cast(
MagicMock, fog_client._cloud_api
).set_fog_platform_device_properties.assert_has_awaits(
[
call(device_id, {"Power": 1}),
call(device_id, {"SetHumidity": 70}),
]
)

@pytest.mark.asyncio
async def test_query_device_state(self, fog_client: DeyeFogMqttClient) -> None:
"""Test query_device_state method."""
Expand Down
Loading