mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-22 19:23:34 +00:00
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:
parent
6cdbbefb90
commit
867b7b8830
@ -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.
|
||||||
|
33
kasa/cli.py
33
kasa/cli.py
@ -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)
|
||||||
|
@ -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(),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user