From e503f672d1e22f23c6fc6d2ec58bfe824930168b Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Wed, 8 May 2024 19:58:08 +0200 Subject: [PATCH 1/7] Add time sync command Fixes also setting the time. --- kasa/cli.py | 35 +++++++++++++++++++++++++++++++++-- kasa/smart/modules/time.py | 8 +++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/kasa/cli.py b/kasa/cli.py index 8919f174d..f9232a9d2 100755 --- a/kasa/cli.py +++ b/kasa/cli.py @@ -967,15 +967,46 @@ async def led(dev: Device, state): return led.led -@cli.command() +@cli.group(invoke_without_command=True) +@click.pass_context +async def time(ctx: click.Context): + """Get and set time.""" + if ctx.invoked_subcommand is None: + await ctx.invoke(time_get) + + +@time.command(name="get") @pass_dev -async def time(dev): +async def time_get(dev: Device): """Get the device time.""" res = dev.time echo(f"Current time: {res}") return res +@time.command(name="sync") +@pass_dev +async def time_sync(dev: SmartDevice): + """Set the device time to current time.""" + if not isinstance(dev, SmartDevice): + raise NotImplementedError("setting time currently only implemented on smart") + + from datetime import datetime + + from .smart.modules import TimeModule + + if (time := dev.get_module(TimeModule)) is None: + echo("Device does not have timemodule") + return + + echo("Old time: %s" % time.time) + + await time.set_time(datetime.now(tz=time.time.tzinfo)) + + await dev.update() + echo("New time: %s" % time.time) + + @cli.command() @click.option("--index", type=int, required=False) @click.option("--name", type=str, required=False) diff --git a/kasa/smart/modules/time.py b/kasa/smart/modules/time.py index 958cf9e21..3c2b96af3 100644 --- a/kasa/smart/modules/time.py +++ b/kasa/smart/modules/time.py @@ -51,7 +51,13 @@ def time(self) -> datetime: async def set_time(self, dt: datetime): """Set device time.""" unixtime = mktime(dt.timetuple()) + offset = cast(timedelta, dt.utcoffset()) + diff = offset / timedelta(minutes=1) return await self.call( "set_device_time", - {"timestamp": unixtime, "time_diff": dt.utcoffset(), "region": dt.tzname()}, + { + "timestamp": int(unixtime), + "time_diff": int(diff), + "region": dt.tzname(), + }, ) From 75daaf4589b1679da0425db4d5b211519090efb2 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Wed, 5 Jun 2024 13:21:16 +0200 Subject: [PATCH 2/7] Make mypy happy --- kasa/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kasa/cli.py b/kasa/cli.py index f9232a9d2..befde8a14 100755 --- a/kasa/cli.py +++ b/kasa/cli.py @@ -993,10 +993,10 @@ async def time_sync(dev: SmartDevice): from datetime import datetime - from .smart.modules import TimeModule + from .smart.modules import Time - if (time := dev.get_module(TimeModule)) is None: - echo("Device does not have timemodule") + if (time := dev.get_module(Time)) is None: + echo("Device does not have time module") return echo("Old time: %s" % time.time) From c54cf2a00d12b53de6fa0b3d95f68dbb36ca99f5 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Wed, 5 Jun 2024 13:21:49 +0200 Subject: [PATCH 3/7] Move datetime import to top --- kasa/cli.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/kasa/cli.py b/kasa/cli.py index befde8a14..e5b4dd5d3 100755 --- a/kasa/cli.py +++ b/kasa/cli.py @@ -9,6 +9,7 @@ import re import sys from contextlib import asynccontextmanager +from datetime import datetime from functools import singledispatch, wraps from pprint import pformat as pf from typing import Any, cast @@ -991,8 +992,6 @@ async def time_sync(dev: SmartDevice): if not isinstance(dev, SmartDevice): raise NotImplementedError("setting time currently only implemented on smart") - from datetime import datetime - from .smart.modules import Time if (time := dev.get_module(Time)) is None: From 52005e9a845ed22ada780335520ef434ca53ff76 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Thu, 6 Jun 2024 00:25:40 +0200 Subject: [PATCH 4/7] Use the proper way to access the time module --- kasa/cli.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/kasa/cli.py b/kasa/cli.py index e5b4dd5d3..aa5d712d3 100755 --- a/kasa/cli.py +++ b/kasa/cli.py @@ -992,9 +992,7 @@ async def time_sync(dev: SmartDevice): if not isinstance(dev, SmartDevice): raise NotImplementedError("setting time currently only implemented on smart") - from .smart.modules import Time - - if (time := dev.get_module(Time)) is None: + if (time := dev.modules.get(Module.Time)) is None: echo("Device does not have time module") return From c115bdcc1fbcfd1a1078b693da3ba44d48ec96d3 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Fri, 14 Jun 2024 23:31:22 +0200 Subject: [PATCH 5/7] Add a note about time syncing to docs --- docs/source/cli.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/source/cli.rst b/docs/source/cli.rst index dad754d25..7d4eb0806 100644 --- a/docs/source/cli.rst +++ b/docs/source/cli.rst @@ -58,6 +58,11 @@ As with all other commands, you can also pass ``--help`` to both ``join`` and `` However, note that communications with devices provisioned using this method will stop working when connected to the cloud. +.. note:: + + Some commands do not work if the device time is out-of-sync. + You can use ``kasa time sync`` command to set the device time from the system where the command is run. + .. warning:: At least some devices (e.g., Tapo lights L530 and L900) are known to have a watchdog that reboots them every 10 minutes if they are unable to connect to the cloud. From 8b57d7e294de0ff0541af229c83501d0f7fb418c Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Fri, 14 Jun 2024 23:41:58 +0200 Subject: [PATCH 6/7] Add tests --- kasa/tests/test_cli.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/kasa/tests/test_cli.py b/kasa/tests/test_cli.py index 2104de050..41b1e1ad9 100644 --- a/kasa/tests/test_cli.py +++ b/kasa/tests/test_cli.py @@ -31,6 +31,7 @@ state, sysinfo, temperature, + time, toggle, update_credentials, wifi, @@ -260,6 +261,37 @@ async def test_update_credentials(dev, runner): ) +async def test_time_get(dev, runner): + """Test time get command.""" + res = await runner.invoke( + time, + obj=dev, + ) + assert res.exit_code == 0 + assert "Current time: " in res.output + + +@device_smart +async def test_time_sync(dev, mocker, runner): + """Test time sync command. + + Currently implemented only for SMART. + """ + update = mocker.patch.object(dev, "update") + set_time_mock = mocker.spy(dev.modules[Module.Time], "set_time") + res = await runner.invoke( + time, + ["sync"], + obj=dev, + ) + set_time_mock.assert_called() + update.assert_called() + + assert res.exit_code == 0 + assert "Old time: " in res.output + assert "New time: " in res.output + + async def test_emeter(dev: Device, mocker, runner): res = await runner.invoke(emeter, obj=dev) if not dev.has_emeter: From e8d4a6880942403943de2721a5d9744e94255ff1 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Sun, 16 Jun 2024 00:12:30 +0200 Subject: [PATCH 7/7] Use local timezone instead of device-defined one --- kasa/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kasa/cli.py b/kasa/cli.py index aa5d712d3..4514c9a5a 100755 --- a/kasa/cli.py +++ b/kasa/cli.py @@ -998,7 +998,8 @@ async def time_sync(dev: SmartDevice): echo("Old time: %s" % time.time) - await time.set_time(datetime.now(tz=time.time.tzinfo)) + local_tz = datetime.now().astimezone().tzinfo + await time.set_time(datetime.now(tz=local_tz)) await dev.update() echo("New time: %s" % time.time)