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
10 changes: 9 additions & 1 deletion kasa/iot/iotdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,15 @@ async def _modular_update(self, req: dict) -> None:
# every other update will query for them
update: dict = self._last_update.copy() if self._last_update else {}
for response in responses:
update = {**update, **response}
for k, v in response.items():
# The same module could have results in different responses
# i.e. smartlife.iot.common.schedule for Usage and
# Schedule, so need to call update(**v) here. If a module is
# not supported the response
# {'err_code': -1, 'err_msg': 'module not support'}
# become top level key/values of the response so check for dict
if isinstance(v, dict):
update.setdefault(k, {}).update(**v)
self._last_update = update

# IOT modules are added as default but could be unsupported post first update
Expand Down
26 changes: 14 additions & 12 deletions kasa/iot/modules/rulemodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
from __future__ import annotations

import logging
from dataclasses import dataclass
from enum import Enum

from pydantic.v1 import BaseModel
from mashumaro import DataClassDictMixin

from ..iotmodule import IotModule, merge

Expand All @@ -28,26 +29,27 @@ class TimeOption(Enum):
AtSunset = 2


class Rule(BaseModel):
@dataclass
class Rule(DataClassDictMixin):
"""Representation of a rule."""

id: str
name: str
enable: bool
enable: int
wday: list[int]
repeat: bool
repeat: int

# start action
sact: Action | None
stime_opt: TimeOption
smin: int
sact: Action | None = None
stime_opt: TimeOption | None = None
smin: int | None = None

eact: Action | None
etime_opt: TimeOption
emin: int
eact: Action | None = None
etime_opt: TimeOption | None = None
emin: int | None = None

# Only on bulbs
s_light: dict | None
s_light: dict | None = None


_LOGGER = logging.getLogger(__name__)
Expand All @@ -66,7 +68,7 @@ def rules(self) -> list[Rule]:
"""Return the list of rules for the service."""
try:
return [
Rule.parse_obj(rule) for rule in self.data["get_rules"]["rule_list"]
Rule.from_dict(rule) for rule in self.data["get_rules"]["rule_list"]
]
except Exception as ex:
_LOGGER.error("Unable to read rule list: %s (data: %s)", ex, self.data)
Expand Down
30 changes: 30 additions & 0 deletions tests/fakeprotocol_iot.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,34 @@ def success(res):
}
}

SCHEDULE_MODULE = {
"get_next_action": {
"action": 1,
"err_code": 0,
"id": "0794F4729DB271627D1CF35A9A854030",
"schd_time": 68927,
"type": 2,
},
"get_rules": {
"enable": 1,
"err_code": 0,
"rule_list": [
{
"eact": -1,
"enable": 1,
"id": "8AA75A50A8440B17941D192BD9E01FFA",
"name": "name",
"repeat": 1,
"sact": 1,
"smin": 1027,
"soffset": 0,
"stime_opt": 2,
"wday": [1, 1, 1, 1, 1, 1, 1],
},
],
"version": 2,
},
}

AMBIENT_MODULE = {
"get_current_brt": {"value": 26, "err_code": 0},
Expand Down Expand Up @@ -450,6 +478,8 @@ def set_time(self, new_state: dict, *args):
"smartlife.iot.PIR": MOTION_MODULE,
"cnCloud": CLOUD_MODULE,
"smartlife.iot.common.cloud": CLOUD_MODULE,
"schedule": SCHEDULE_MODULE,
"smartlife.iot.common.schedule": SCHEDULE_MODULE,
}

async def send(self, request, port=9999):
Expand Down
17 changes: 17 additions & 0 deletions tests/iot/modules/test_schedule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import pytest

from kasa import Device, Module
from kasa.iot.modules.rulemodule import Action, TimeOption

from ...device_fixtures import device_iot


@device_iot
def test_schedule(dev: Device, caplog: pytest.LogCaptureFixture):
schedule = dev.modules.get(Module.IotSchedule)
assert schedule
if rules := schedule.rules:
first = rules[0]
assert isinstance(first.sact, Action)
assert isinstance(first.stime_opt, TimeOption)
assert "Unable to read rule list" not in caplog.text
Loading