Add time sync command (#951)

Allows setting the device time (on SMART devices) to the current time.
Fixes also setting the time which was previously broken.
This commit is contained in:
Teemu R 2024-06-17 10:37:08 +02:00 committed by GitHub
parent 6cdbbefb90
commit 867b7b8830
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 75 additions and 3 deletions

View File

@ -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 However, note that communications with devices provisioned using this method will stop working
when connected to the cloud. 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:: .. 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. 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.

View File

@ -9,6 +9,7 @@ import logging
import re import re
import sys import sys
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from datetime import datetime
from functools import singledispatch, wraps from functools import singledispatch, wraps
from pprint import pformat as pf from pprint import pformat as pf
from typing import Any, cast from typing import Any, cast
@ -967,15 +968,43 @@ async def led(dev: Device, state):
return led.led 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 @pass_dev
async def time(dev): async def time_get(dev: Device):
"""Get the device time.""" """Get the device time."""
res = dev.time res = dev.time
echo(f"Current time: {res}") echo(f"Current time: {res}")
return 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")
if (time := dev.modules.get(Module.Time)) is None:
echo("Device does not have time module")
return
echo("Old time: %s" % time.time)
local_tz = datetime.now().astimezone().tzinfo
await time.set_time(datetime.now(tz=local_tz))
await dev.update()
echo("New time: %s" % time.time)
@cli.command() @cli.command()
@click.option("--index", type=int, required=False) @click.option("--index", type=int, required=False)
@click.option("--name", type=str, required=False) @click.option("--name", type=str, required=False)

View File

@ -51,7 +51,13 @@ class Time(SmartModule):
async def set_time(self, dt: datetime): async def set_time(self, dt: datetime):
"""Set device time.""" """Set device time."""
unixtime = mktime(dt.timetuple()) unixtime = mktime(dt.timetuple())
offset = cast(timedelta, dt.utcoffset())
diff = offset / timedelta(minutes=1)
return await self.call( return await self.call(
"set_device_time", "set_device_time",
{"timestamp": unixtime, "time_diff": dt.utcoffset(), "region": dt.tzname()}, {
"timestamp": int(unixtime),
"time_diff": int(diff),
"region": dt.tzname(),
},
) )

View File

@ -31,6 +31,7 @@ from kasa.cli import (
state, state,
sysinfo, sysinfo,
temperature, temperature,
time,
toggle, toggle,
update_credentials, update_credentials,
wifi, 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): async def test_emeter(dev: Device, mocker, runner):
res = await runner.invoke(emeter, obj=dev) res = await runner.invoke(emeter, obj=dev)
if not dev.has_emeter: if not dev.has_emeter: