Skip to content
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

Add time sync command #951

Merged
merged 7 commits into from
Jun 17, 2024
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
5 changes: 5 additions & 0 deletions docs/source/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
33 changes: 31 additions & 2 deletions kasa/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -967,15 +968,43 @@
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")

if (time := dev.modules.get(Module.Time)) is None:
echo("Device does not have time module")
return

Check warning on line 997 in kasa/cli.py

View check run for this annotation

Codecov / codecov/patch

kasa/cli.py#L996-L997

Added lines #L996 - L997 were not covered by tests

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()
@click.option("--index", type=int, required=False)
@click.option("--name", type=str, required=False)
Expand Down
8 changes: 7 additions & 1 deletion kasa/smart/modules/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Comment on lines +59 to +60
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was not ever working previously due to incorrect types.

"region": dt.tzname(),
},
)
32 changes: 32 additions & 0 deletions kasa/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
state,
sysinfo,
temperature,
time,
toggle,
update_credentials,
wifi,
Expand Down Expand Up @@ -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:
Expand Down
Loading