10000 Add module support & query their information during update cycle by rytilahti · Pull Request #243 · python-kasa/python-kasa · GitHub
[go: up one dir, main page]

Skip to content

Add module support & query their information during update cycle #243

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Nov 7, 2021
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
29 changes: 27 additions & 2 deletions kasa/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ async def state(ctx, dev: SmartDevice):
click.echo()

click.echo(click.style("\t== Generic information ==", bold=True))
click.echo(f"\tTime: {await dev.get_time()}")
click.echo(f"\tTime: {dev.time} (tz: {dev.timezone}")
click.echo(f"\tHardware: {dev.hw_info['hw_ver']}")
click.echo(f"\tSoftware: {dev.hw_info['sw_ver']}")
click.echo(f"\tMAC (rssi): {dev.mac} ({dev.rssi})")
Expand All @@ -215,6 +215,13 @@ async def state(ctx, dev: SmartDevice):
emeter_status = dev.emeter_realtime
click.echo(f"\t{emeter_status}")

click.echo(click.style("\n\t== Modules ==", bold=True))
for module in dev.modules.values():
if module.is_supported:
click.echo(click.style(f"\t+ {module}", fg="green"))
else:
click.echo(click.style(f"\t- {module}", fg="red"))


@cli.command()
@pass_dev
Expand Down Expand Up @@ -388,7 +395,7 @@ async def led(dev, state):
@pass_dev
async def time(dev):
"""Get the device time."""
res = await dev.get_time()
res = dev.time
click.echo(f"Current time: {res}")
return res

Expand Down Expand Up @@ -446,5 +453,23 @@ async def reboot(plug, delay):
return await plug.reboot(delay)


@cli.group()
@pass_dev
async def schedule(dev):
"""Scheduling commands."""


@schedule.command(name="list")
@pass_dev
@click.argument("type", default="schedule")
def _schedule_list(dev, type):
"""Return the list of schedule actions for the given type."""
sched = dev.modules[type]
for rule in sched.rules:
print(rule)
else:
click.echo(f"No rules of type {type}")


if __name__ == "__main__":
cli()
10 changes: 10 additions & 0 deletions kasa/modules/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# flake8: noqa
from .antitheft import Antitheft
from .cloud import Cloud
from .countdown import Countdown
from .emeter import Emeter
from .module import Module
from .rulemodule import Rule, RuleModule
from .schedule import Schedule
from .time import Time
from .usage import Usage
9 changes: 9 additions & 0 deletions kasa/modules/antitheft.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Implementation of the antitheft module."""
from .rulemodule import RuleModule


class Antitheft(RuleModule):
"""Implementation of the antitheft module.

This shares the functionality among other rule-based modules.
"""
50 changes: 50 additions & 0 deletions kasa/modules/cloud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Cloud module implementation."""
from pydantic import BaseModel

from .module import Module


class CloudInfo(BaseModel):
"""Container for cloud settings."""

binded: bool
cld_connection: int
fwDlPage: str
fwNotifyType: int
illegalType: int
server: str
stopConnect: int
tcspInfo: str
tcspStatus: int
username: str


class Cloud(Module):
"""Module implementing support for cloud services."""

def query(self):
"""Request cloud connectivity info."""
return self.query_for_command("get_info")

@property
def info(self) -> CloudInfo:
"""Return information about the cloud connectivity."""
return CloudInfo.parse_obj(self.data["get_info"])

def get_available_firmwares(self):
"""Return list of available firmwares."""
return self.query_for_command("get_intl_fw_list")

def set_server(self, url: str):
"""Set the update server URL."""
return self.query_for_command("set_server_url", {"server": url})

def connect(self, username: str, password: str):
"""Login to the cloud using given information."""
return self.query_for_command(
"bind", {"username": username, "password": password}
)

def disconnect(self):
"""Disconnect from the cloud."""
return self.query_for_command("unbind")
6 changes: 6 additions & 0 deletions kasa/modules/countdown.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Implementation for the countdown timer."""
from .rulemodule import RuleModule


class Countdown(RuleModule):
"""Implementation of countdown module."""
20 changes: 20 additions & 0 deletions kasa/modules/emeter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Implementation of the emeter module."""
from ..emeterstatus import EmeterStatus
from .usage import Usage


class Emeter(Usage):
"""Emeter module."""

def query(self):
"""Prepare query for emeter data."""
return self._device._create_emeter_request()

@property # type: ignore
def realtime(self) -> EmeterStatus:
"""Return current energy readings."""
return EmeterStatus(self.data["get_realtime"])

async def erase_stats(self):
"""Erase all stats."""
return await self.call("erase_emeter_stat")
59 changes: 59 additions & 0 deletions kasa/modules/module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Base class for all module implementations."""
import collections
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from kasa import SmartDevice


# TODO: This is used for query construcing
def merge(d, u):
"""Update dict recursively."""
for k, v in u.items():
if isinstance(v, collections.abc.Mapping):
d[k] = merge(d.get(k, {}), v)
else:
d[k] = v
return d


class Module(ABC):
"""Base class implemention for all modules.

The base classes should implement `query` to return the query they want to be
executed during the regular update cycle.
"""

def __init__(self, device: "SmartDevice", module: str):
self._device: "SmartDevice" = device
self._module = module

@abstractmethod
def query(self):
"""Query to execute during the update cycle.

The inheriting modules implement this to include their wanted
queries to the query that gets executed when Device.update() gets called.
"""

@property
def data(self):
"""Return the module specific raw data from the last update."""
return self._device._last_update[self._module]

@property
def is_supported(self) -> bool:
"""Return whether the module is supported by the device."""
return "err_code" not in self.data

def call(self, method, params=None):
"""Call the given method with the given parameters."""
return self._device._query_helper(self._module, method, params)

def query_for_command(self, query, params=None):
"""Create a request object for the given parameters."""
return self._device._create_request(self._module, query, params)

def __repr__(self) -> str:
return f"<Module {self.__class__.__name__} ({self._module}) for {self._device.host}>"
83 changes: 83 additions & 0 deletions kasa/modules/rulemodule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Base implementation for all rule-based modules."""
import logging
from enum import Enum
from typing import Dict, List, Optional

from pydantic import BaseModel

from .module import Module, merge


class Action(Enum):
"""Action to perform."""

Disabled = -1
TurnOff = 0
TurnOn = 1
Unknown = 2


class TimeOption(Enum):
"""Time when the action is executed."""

Disabled = -1
Enabled = 0
AtSunrise = 1
AtSunset = 2


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

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

# start action
sact: Optional[Action]
stime_opt: TimeOption
smin: int

eact: Optional[Action]
etime_opt: TimeOption
emin: int

# Only on bulbs
s_light: Optional[Dict]


_LOGGER = logging.getLogger(__name__)


class RuleModule(Module):
"""Base class for rule-based modules, such as countdown and antitheft."""

def query(self):
"""Prepare the query for rules."""
q = self.query_for_command("get_rules")
return merge(q, self.query_for_command("get_next_action"))

@property
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"]
]
except Exception as ex:
_LOGGER.error("Unable to read rule list: %s (data: %s)", ex, self.data)
return []

async def set_enabled(self, state: bool):
"""Enable or disable the service."""
return await self.call("set_overall_enable", state)

async def delete_rule(self, rule: Rule):
"""Delete the given rule."""
return await self.call("delete_rule", {"id": rule.id})

async def delete_all_rules(self):
"""Delete all rules."""
return await self.call("delete_all_rules")
6 changes: 6 additions & 0 deletions kasa/modules/schedule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Schedule module implementation."""
from .rulemodule import RuleModule


class Schedule(RuleModule):
"""Implements the scheduling interface."""
34 changes: 34 additions & 0 deletions kasa/modules/time.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Provides the current time and timezone information."""
from datetime import datetime

from .module import Module, merge


class Time(Module):
"""Implements the timezone settings."""

def query(self):
"""Request time and timezone."""
q = self.query_for_command("get_time")

merge(q, self.query_for_command("get_timezone"))
return q

@property
def time(self) -> datetime:
"""Return current device time."""
res = self.data["get_time"]
return datetime(
res["year"],
res["month"],
res["mday"],
res["hour"],
res["min"],
res["sec"],
)

@property
def timezone(self):
"""Return current timezone."""
res = self.data["get_timezone"]
return res
40 changes: 40 additions & 0 deletions kasa/modules/usage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Implementation of the usage interface."""
from datetime import datetime

from .module import Module, merge


class Usage(Module):
"""Baseclass for emeter/usage interfaces."""

def query(self):
"""Return the base query."""
year = datetime.now().year
month = datetime.now().month

req = self.query_for_command("get_realtime")
req = merge(
req, self.query_for_command("get_daystat", {"year": year, "month": month})
)
req = merge(req, self.query_for_command("get_monthstat", {"year": year}))
req = merge(req, self.query_for_command("get_next_action"))

return req

async def get_daystat(self, year, month):
"""Return stats for the current day."""
if year is None:
year = datetime.now().year
if month is None:
month = datetime.now().month
return await self.call("get_daystat", {"year": year, "month": month})

async def get_monthstat(self, year):
"""Return stats for the current month."""
if year is None:
year = datetime.now().year
return await self.call("get_monthstat", {"year": year})

async def erase_stats(self):
"""Erase all stats."""
return await self.call("erase_runtime_stat")
Loading
0