mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-08-06 18:54:08 +00:00
Refactor devices into subpackages and deprecate old names (#716)
* Refactor devices into subpackages and deprecate old names * Tweak and add tests * Fix linting * Remove duplicate implementations affecting project coverage * Update post review * Add device base class attributes and rename subclasses * Rename Module to BaseModule * Remove has_emeter_history * Fix missing _time in init * Update post review * Fix test_readmeexamples * Fix erroneously duped files * Clean up iot and smart imports * Update post latest review * Tweak Device docstring
This commit is contained in:
27
kasa/iot/modules/__init__.py
Normal file
27
kasa/iot/modules/__init__.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""Module for individual feature modules."""
|
||||
from .ambientlight import AmbientLight
|
||||
from .antitheft import Antitheft
|
||||
from .cloud import Cloud
|
||||
from .countdown import Countdown
|
||||
from .emeter import Emeter
|
||||
from .module import IotModule
|
||||
from .motion import Motion
|
||||
from .rulemodule import Rule, RuleModule
|
||||
from .schedule import Schedule
|
||||
from .time import Time
|
||||
from .usage import Usage
|
||||
|
||||
__all__ = [
|
||||
"AmbientLight",
|
||||
"Antitheft",
|
||||
"Cloud",
|
||||
"Countdown",
|
||||
"Emeter",
|
||||
"IotModule",
|
||||
"Motion",
|
||||
"Rule",
|
||||
"RuleModule",
|
||||
"Schedule",
|
||||
"Time",
|
||||
"Usage",
|
||||
]
|
47
kasa/iot/modules/ambientlight.py
Normal file
47
kasa/iot/modules/ambientlight.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""Implementation of the ambient light (LAS) module found in some dimmers."""
|
||||
from .module import IotModule
|
||||
|
||||
# TODO create tests and use the config reply there
|
||||
# [{"hw_id":0,"enable":0,"dark_index":1,"min_adc":0,"max_adc":2450,
|
||||
# "level_array":[{"name":"cloudy","adc":490,"value":20},
|
||||
# {"name":"overcast","adc":294,"value":12},
|
||||
# {"name":"dawn","adc":222,"value":9},
|
||||
# {"name":"twilight","adc":222,"value":9},
|
||||
# {"name":"total darkness","adc":111,"value":4},
|
||||
# {"name":"custom","adc":2400,"value":97}]}]
|
||||
|
||||
|
||||
class AmbientLight(IotModule):
|
||||
"""Implements ambient light controls for the motion sensor."""
|
||||
|
||||
def query(self):
|
||||
"""Request configuration."""
|
||||
return self.query_for_command("get_config")
|
||||
|
||||
@property
|
||||
def presets(self) -> dict:
|
||||
"""Return device-defined presets for brightness setting."""
|
||||
return self.data["level_array"]
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
"""Return True if the module is enabled."""
|
||||
return bool(self.data["enable"])
|
||||
|
||||
async def set_enabled(self, state: bool):
|
||||
"""Enable/disable LAS."""
|
||||
return await self.call("set_enable", {"enable": int(state)})
|
||||
|
||||
async def current_brightness(self) -> int:
|
||||
"""Return current brightness.
|
||||
|
||||
Return value units.
|
||||
"""
|
||||
return await self.call("get_current_brt")
|
||||
|
||||
async def set_brightness_limit(self, value: int):
|
||||
"""Set the limit when the motion sensor is inactive.
|
||||
|
||||
See `presets` for preset values. Custom values are also likely allowed.
|
||||
"""
|
||||
return await self.call("set_brt_level", {"index": 0, "value": value})
|
9
kasa/iot/modules/antitheft.py
Normal file
9
kasa/iot/modules/antitheft.py
Normal file
@@ -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.
|
||||
"""
|
53
kasa/iot/modules/cloud.py
Normal file
53
kasa/iot/modules/cloud.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Cloud module implementation."""
|
||||
try:
|
||||
from pydantic.v1 import BaseModel
|
||||
except ImportError:
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .module import IotModule
|
||||
|
||||
|
||||
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(IotModule):
|
||||
"""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
kasa/iot/modules/countdown.py
Normal file
6
kasa/iot/modules/countdown.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Implementation for the countdown timer."""
|
||||
from .rulemodule import RuleModule
|
||||
|
||||
|
||||
class Countdown(RuleModule):
|
||||
"""Implementation of countdown module."""
|
111
kasa/iot/modules/emeter.py
Normal file
111
kasa/iot/modules/emeter.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""Implementation of the emeter module."""
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
from ...emeterstatus import EmeterStatus
|
||||
from .usage import Usage
|
||||
|
||||
|
||||
class Emeter(Usage):
|
||||
"""Emeter module."""
|
||||
|
||||
@property # type: ignore
|
||||
def realtime(self) -> EmeterStatus:
|
||||
"""Return current energy readings."""
|
||||
return EmeterStatus(self.data["get_realtime"])
|
||||
|
||||
@property
|
||||
def emeter_today(self) -> Optional[float]:
|
||||
"""Return today's energy consumption in kWh."""
|
||||
raw_data = self.daily_data
|
||||
today = datetime.now().day
|
||||
data = self._convert_stat_data(raw_data, entry_key="day", key=today)
|
||||
return data.get(today)
|
||||
|
||||
@property
|
||||
def emeter_this_month(self) -> Optional[float]:
|
||||
"""Return this month's energy consumption in kWh."""
|
||||
raw_data = self.monthly_data
|
||||
current_month = datetime.now().month
|
||||
data = self._convert_stat_data(raw_data, entry_key="month", key=current_month)
|
||||
return data.get(current_month)
|
||||
|
||||
async def erase_stats(self):
|
||||
"""Erase all stats.
|
||||
|
||||
Uses different query than usage meter.
|
||||
"""
|
||||
return await self.call("erase_emeter_stat")
|
||||
|
||||
async def get_realtime(self):
|
||||
"""Return real-time statistics."""
|
||||
return await self.call("get_realtime")
|
||||
|
||||
async def get_daystat(self, *, year=None, month=None, kwh=True) -> Dict:
|
||||
"""Return daily stats for the given year & month.
|
||||
|
||||
The return value is a dictionary of {day: energy, ...}.
|
||||
"""
|
||||
data = await self.get_raw_daystat(year=year, month=month)
|
||||
data = self._convert_stat_data(data["day_list"], entry_key="day", kwh=kwh)
|
||||
return data
|
||||
|
||||
async def get_monthstat(self, *, year=None, kwh=True) -> Dict:
|
||||
"""Return monthly stats for the given year.
|
||||
|
||||
The return value is a dictionary of {month: energy, ...}.
|
||||
"""
|
||||
data = await self.get_raw_monthstat(year=year)
|
||||
data = self._convert_stat_data(data["month_list"], entry_key="month", kwh=kwh)
|
||||
return data
|
||||
|
||||
def _convert_stat_data(
|
||||
self,
|
||||
data: List[Dict[str, Union[int, float]]],
|
||||
entry_key: str,
|
||||
kwh: bool = True,
|
||||
key: Optional[int] = None,
|
||||
) -> Dict[Union[int, float], Union[int, float]]:
|
||||
"""Return emeter information keyed with the day/month.
|
||||
|
||||
The incoming data is a list of dictionaries::
|
||||
|
||||
[{'year': int,
|
||||
'month': int,
|
||||
'day': int, <-- for get_daystat not get_monthstat
|
||||
'energy_wh': int, <-- for emeter in some versions (wh)
|
||||
'energy': float <-- for emeter in other versions (kwh)
|
||||
}, ...]
|
||||
|
||||
:return: a dictionary keyed by day or month with energy as the value.
|
||||
"""
|
||||
if not data:
|
||||
return {}
|
||||
|
||||
scale: float = 1
|
||||
|
||||
if "energy_wh" in data[0]:
|
||||
value_key = "energy_wh"
|
||||
if kwh:
|
||||
scale = 1 / 1000
|
||||
else:
|
||||
value_key = "energy"
|
||||
if not kwh:
|
||||
scale = 1000
|
||||
|
||||
if key is None:
|
||||
# Return all the data
|
||||
return {entry[entry_key]: entry[value_key] * scale for entry in data}
|
||||
|
||||
# In this case we want a specific key in the data
|
||||
# i.e. the current day or month.
|
||||
#
|
||||
# Since we usually want the data at the end of the list so we can
|
||||
# optimize the search by starting at the end and avoid scaling
|
||||
# the data we don't need.
|
||||
#
|
||||
for entry in reversed(data):
|
||||
if entry[entry_key] == key:
|
||||
return {entry[entry_key]: entry[value_key] * scale}
|
||||
|
||||
return {}
|
87
kasa/iot/modules/module.py
Normal file
87
kasa/iot/modules/module.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""Base class for all module implementations."""
|
||||
import collections
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ...exceptions import SmartDeviceException
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from kasa.iot import IotDevice
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 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 IotModule(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: "IotDevice", module: str):
|
||||
self._device = 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 estimated_query_response_size(self):
|
||||
"""Estimated maximum size of query response.
|
||||
|
||||
The inheriting modules implement this to estimate how large a query response
|
||||
will be so that queries can be split should an estimated response be too large
|
||||
"""
|
||||
return 256 # Estimate for modules that don't specify
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
"""Return the module specific raw data from the last update."""
|
||||
if self._module not in self._device._last_update:
|
||||
raise SmartDeviceException(
|
||||
f"You need to call update() prior accessing module data"
|
||||
f" for '{self._module}'"
|
||||
)
|
||||
|
||||
return self._device._last_update[self._module]
|
||||
|
||||
@property
|
||||
def is_supported(self) -> bool:
|
||||
"""Return whether the module is supported by the device."""
|
||||
if self._module not in self._device._last_update:
|
||||
_LOGGER.debug("Initial update, so consider supported: %s", self._module)
|
||||
return True
|
||||
|
||||
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})"
|
||||
f" for {self._device.host}>"
|
||||
)
|
74
kasa/iot/modules/motion.py
Normal file
74
kasa/iot/modules/motion.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""Implementation of the motion detection (PIR) module found in some dimmers."""
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from ...exceptions import SmartDeviceException
|
||||
from .module import IotModule
|
||||
|
||||
|
||||
class Range(Enum):
|
||||
"""Range for motion detection."""
|
||||
|
||||
Far = 0
|
||||
Mid = 1
|
||||
Near = 2
|
||||
Custom = 3
|
||||
|
||||
|
||||
# TODO: use the config reply in tests
|
||||
# {"enable":0,"version":"1.0","trigger_index":2,"cold_time":60000,
|
||||
# "min_adc":0,"max_adc":4095,"array":[80,50,20,0],"err_code":0}}}
|
||||
|
||||
|
||||
class Motion(IotModule):
|
||||
"""Implements the motion detection (PIR) module."""
|
||||
|
||||
def query(self):
|
||||
"""Request PIR configuration."""
|
||||
return self.query_for_command("get_config")
|
||||
|
||||
@property
|
||||
def range(self) -> Range:
|
||||
"""Return motion detection range."""
|
||||
return Range(self.data["trigger_index"])
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
"""Return True if module is enabled."""
|
||||
return bool(self.data["enable"])
|
||||
|
||||
async def set_enabled(self, state: bool):
|
||||
"""Enable/disable PIR."""
|
||||
return await self.call("set_enable", {"enable": int(state)})
|
||||
|
||||
async def set_range(
|
||||
self, *, range: Optional[Range] = None, custom_range: Optional[int] = None
|
||||
):
|
||||
"""Set the range for the sensor.
|
||||
|
||||
:param range: for using standard ranges
|
||||
:param custom_range: range in decimeters, overrides the range parameter
|
||||
"""
|
||||
if custom_range is not None:
|
||||
payload = {"index": Range.Custom.value, "value": custom_range}
|
||||
elif range is not None:
|
||||
payload = {"index": range.value}
|
||||
else:
|
||||
raise SmartDeviceException(
|
||||
"Either range or custom_range need to be defined"
|
||||
)
|
||||
|
||||
return await self.call("set_trigger_sens", payload)
|
||||
|
||||
@property
|
||||
def inactivity_timeout(self) -> int:
|
||||
"""Return inactivity timeout in milliseconds."""
|
||||
return self.data["cold_time"]
|
||||
|
||||
async def set_inactivity_timeout(self, timeout: int):
|
||||
"""Set inactivity timeout in milliseconds.
|
||||
|
||||
Note, that you need to delete the default "Smart Control" rule in the app
|
||||
to avoid reverting this back to 60 seconds after a period of time.
|
||||
"""
|
||||
return await self.call("set_cold_time", {"cold_time": timeout})
|
87
kasa/iot/modules/rulemodule.py
Normal file
87
kasa/iot/modules/rulemodule.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""Base implementation for all rule-based modules."""
|
||||
import logging
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
try:
|
||||
from pydantic.v1 import BaseModel
|
||||
except ImportError:
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
from .module import IotModule, 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(IotModule):
|
||||
"""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
kasa/iot/modules/schedule.py
Normal file
6
kasa/iot/modules/schedule.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Schedule module implementation."""
|
||||
from .rulemodule import RuleModule
|
||||
|
||||
|
||||
class Schedule(RuleModule):
|
||||
"""Implements the scheduling interface."""
|
54
kasa/iot/modules/time.py
Normal file
54
kasa/iot/modules/time.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""Provides the current time and timezone information."""
|
||||
from datetime import datetime
|
||||
|
||||
from ...exceptions import SmartDeviceException
|
||||
from .module import IotModule, merge
|
||||
|
||||
|
||||
class Time(IotModule):
|
||||
"""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
|
||||
|
||||
async def get_time(self):
|
||||
"""Return current device time."""
|
||||
try:
|
||||
res = await self.call("get_time")
|
||||
return datetime(
|
||||
res["year"],
|
||||
res["month"],
|
||||
res["mday"],
|
||||
res["hour"],
|
||||
res["min"],
|
||||
res["sec"],
|
||||
)
|
||||
except SmartDeviceException:
|
||||
return None
|
||||
|
||||
async def get_timezone(self):
|
||||
"""Request timezone information from the device."""
|
||||
return await self.call("get_timezone")
|
116
kasa/iot/modules/usage.py
Normal file
116
kasa/iot/modules/usage.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""Implementation of the usage interface."""
|
||||
from datetime import datetime
|
||||
from typing import Dict
|
||||
|
||||
from .module import IotModule, merge
|
||||
|
||||
|
||||
class Usage(IotModule):
|
||||
"""Baseclass for emeter/usage interfaces."""
|
||||
|
||||
def query(self):
|
||||
"""Return the base query."""
|
||||
now = datetime.now()
|
||||
year = now.year
|
||||
month = 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}))
|
||||
|
||||
return req
|
||||
|
||||
@property
|
||||
def estimated_query_response_size(self):
|
||||
"""Estimated maximum query response size."""
|
||||
return 2048
|
||||
|
||||
@property
|
||||
def daily_data(self):
|
||||
"""Return statistics on daily basis."""
|
||||
return self.data["get_daystat"]["day_list"]
|
||||
|
||||
@property
|
||||
def monthly_data(self):
|
||||
"""Return statistics on monthly basis."""
|
||||
return self.data["get_monthstat"]["month_list"]
|
||||
|
||||
@property
|
||||
def usage_today(self):
|
||||
"""Return today's usage in minutes."""
|
||||
today = datetime.now().day
|
||||
# Traverse the list in reverse order to find the latest entry.
|
||||
for entry in reversed(self.daily_data):
|
||||
if entry["day"] == today:
|
||||
return entry["time"]
|
||||
return None
|
||||
|
||||
@property
|
||||
def usage_this_month(self):
|
||||
"""Return usage in this month in minutes."""
|
||||
this_month = datetime.now().month
|
||||
# Traverse the list in reverse order to find the latest entry.
|
||||
for entry in reversed(self.monthly_data):
|
||||
if entry["month"] == this_month:
|
||||
return entry["time"]
|
||||
return None
|
||||
|
||||
async def get_raw_daystat(self, *, year=None, month=None) -> Dict:
|
||||
"""Return raw daily stats for the given year & month."""
|
||||
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_raw_monthstat(self, *, year=None) -> Dict:
|
||||
"""Return raw monthly stats for the given year."""
|
||||
if year is None:
|
||||
year = datetime.now().year
|
||||
|
||||
return await self.call("get_monthstat", {"year": year})
|
||||
|
||||
async def get_daystat(self, *, year=None, month=None) -> Dict:
|
||||
"""Return daily stats for the given year & month.
|
||||
|
||||
The return value is a dictionary of {day: time, ...}.
|
||||
"""
|
||||
data = await self.get_raw_daystat(year=year, month=month)
|
||||
data = self._convert_stat_data(data["day_list"], entry_key="day")
|
||||
return data
|
||||
|
||||
async def get_monthstat(self, *, year=None) -> Dict:
|
||||
"""Return monthly stats for the given year.
|
||||
|
||||
The return value is a dictionary of {month: time, ...}.
|
||||
"""
|
||||
data = await self.get_raw_monthstat(year=year)
|
||||
data = self._convert_stat_data(data["month_list"], entry_key="month")
|
||||
return data
|
||||
|
||||
async def erase_stats(self):
|
||||
"""Erase all stats."""
|
||||
return await self.call("erase_runtime_stat")
|
||||
|
||||
def _convert_stat_data(self, data, entry_key) -> Dict:
|
||||
"""Return usage information keyed with the day/month.
|
||||
|
||||
The incoming data is a list of dictionaries::
|
||||
|
||||
[{'year': int,
|
||||
'month': int,
|
||||
'day': int, <-- for get_daystat not get_monthstat
|
||||
'time': int, <-- for usage (mins)
|
||||
}, ...]
|
||||
|
||||
:return: return a dictionary keyed by day or month with time as the value.
|
||||
"""
|
||||
if not data:
|
||||
return {}
|
||||
|
||||
data = {entry[entry_key]: entry["time"] for entry in data}
|
||||
|
||||
return data
|
Reference in New Issue
Block a user