mqttcommander 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mqttcommander/Helper.py +37 -0
- mqttcommander/__init__.py +88 -0
- mqttcommander/cli.py +112 -0
- mqttcommander/models.py +201 -0
- mqttcommander/py.typed +0 -0
- mqttcommander/tasmotacommander.py +675 -0
- mqttcommander-0.0.1.dist-info/METADATA +387 -0
- mqttcommander-0.0.1.dist-info/RECORD +11 -0
- mqttcommander-0.0.1.dist-info/WHEEL +4 -0
- mqttcommander-0.0.1.dist-info/entry_points.txt +2 -0
- mqttcommander-0.0.1.dist-info/licenses/LICENSE.md +163 -0
mqttcommander/Helper.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import datetime, date, timedelta
|
|
3
|
+
from typing import Any
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def print_pretty_dict_json(data: Any, indent: int = 4) -> None:
|
|
8
|
+
print(json.dumps(data, indent=indent, sort_keys=True, cls=ComplexEncoder, default=str))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_pretty_dict_json(data: Any, indent: int = 4) -> str:
|
|
12
|
+
return json.dumps(data, indent=indent, sort_keys=True, cls=ComplexEncoder, default=str)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_pretty_dict_json_no_sort(data: Any, indent: int = 4) -> str:
|
|
16
|
+
return json.dumps(data, indent=indent, sort_keys=False, cls=ComplexEncoder, default=str)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ComplexEncoder(json.JSONEncoder):
|
|
20
|
+
def default(self, obj: Any) -> Any:
|
|
21
|
+
if hasattr(obj, "repr_json"):
|
|
22
|
+
return obj.repr_json()
|
|
23
|
+
elif hasattr(obj, "as_string"):
|
|
24
|
+
return obj.as_string()
|
|
25
|
+
elif isinstance(obj, UUID):
|
|
26
|
+
return str(obj)
|
|
27
|
+
elif isinstance(obj, datetime):
|
|
28
|
+
return obj.isoformat() # strftime("%Y-%m-%d %H:%M:%S %Z")
|
|
29
|
+
elif isinstance(obj, date):
|
|
30
|
+
return obj.strftime("%Y-%m-%d")
|
|
31
|
+
elif isinstance(obj, timedelta):
|
|
32
|
+
return str(obj)
|
|
33
|
+
elif isinstance(obj, dict) or isinstance(obj, list):
|
|
34
|
+
robj: str = get_pretty_dict_json_no_sort(obj)
|
|
35
|
+
return robj
|
|
36
|
+
else:
|
|
37
|
+
return json.JSONEncoder.default(self, obj)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Callable, Dict, Any
|
|
6
|
+
|
|
7
|
+
from loguru import logger as glogger
|
|
8
|
+
|
|
9
|
+
glogger.disable(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _loguru_skiplog_filter(record: dict) -> bool:
|
|
13
|
+
"""Filter function to hide records with ``extra['skiplog']`` set.
|
|
14
|
+
|
|
15
|
+
Intended for use with ``loguru``'s ``filter=`` parameter so that callers can
|
|
16
|
+
temporarily suppress verbose logs for specific operations by binding
|
|
17
|
+
``skiplog=True`` on the logger.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
record (dict): Loguru record dictionary.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
bool: ``True`` to keep the record, ``False`` to skip it.
|
|
24
|
+
"""
|
|
25
|
+
# {
|
|
26
|
+
# "elapsed": timedelta, # Zeit seit Programmstart
|
|
27
|
+
# "exception": tuple, # Exception-Info (type, value, traceback) oder None
|
|
28
|
+
# "extra": dict, # Benutzerdefinierte Extra-Felder
|
|
29
|
+
# "file": RecordFile, # Datei-Info (name, path)
|
|
30
|
+
# "function": str, # Name der Funktion
|
|
31
|
+
# "level": RecordLevel, # Level-Info (name, no, icon)
|
|
32
|
+
# "line": int, # Zeilennummer
|
|
33
|
+
# "message": str, # Formatierte Nachricht
|
|
34
|
+
# "module": str, # Modulname
|
|
35
|
+
# "name": str, # Logger-Name
|
|
36
|
+
# "process": RecordProcess, # Process-Info (id, name)
|
|
37
|
+
# "thread": RecordThread, # Thread-Info (id, name)
|
|
38
|
+
# "time": datetime # Zeitstempel des Log-Eintrags
|
|
39
|
+
# }
|
|
40
|
+
return not record.get("extra", {}).get("skiplog", False)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def configure_loguru_default_with_skiplog_filter(
|
|
44
|
+
loguru_filter: Callable[[Dict[str, Any]], bool] = _loguru_skiplog_filter,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Configure a default ``loguru`` sink with a convenient format and filter.
|
|
47
|
+
|
|
48
|
+
This sets a colored formatter, applies the given filter (by default
|
|
49
|
+
:func:`_loguru_skiplog_filter`), and ensures a reasonable default log level
|
|
50
|
+
via the ``LOGURU_LEVEL`` environment variable.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
loguru_filter: A callable taking a record dict and returning ``True``
|
|
54
|
+
if the record should be emitted. Defaults to
|
|
55
|
+
:func:`_loguru_skiplog_filter`.
|
|
56
|
+
"""
|
|
57
|
+
glogger.info("configure_loguru_default_with_skiplog_filter")
|
|
58
|
+
|
|
59
|
+
os.environ["LOGURU_LEVEL"] = os.getenv("LOGURU_LEVEL", "DEBUG") # standard is DEBUG
|
|
60
|
+
glogger.remove() # remove default-handler
|
|
61
|
+
logger_fmt: str = (
|
|
62
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{module}</cyan>::<cyan>{extra[classname]}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>"
|
|
63
|
+
)
|
|
64
|
+
# logger_fmt: str = "<g>{time:HH:mm:ssZZ}</> | <lvl>{level}</> | <c>{module}::{extra[classname]}:{function}:{line}</> - {message}"
|
|
65
|
+
|
|
66
|
+
glogger.add(sys.stderr, level=os.getenv("LOGURU_LEVEL"), format=logger_fmt, filter=loguru_filter) # type: ignore # TRACE | DEBUG | INFO | WARN | ERROR | FATAL
|
|
67
|
+
glogger.configure(extra={"classname": "None", "skiplog": False})
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
from .tasmotacommander import (
|
|
71
|
+
MqttCommander,
|
|
72
|
+
read_tasmotas_from_latest_file,
|
|
73
|
+
write_tasmota_devices_file,
|
|
74
|
+
TASMOTA_DEFAULT_TOPICS,
|
|
75
|
+
TASMOTA_DISCOVERY_TOPIC_BEGIN,
|
|
76
|
+
TASMOTA_LWT_TOPIC_BEGIN,
|
|
77
|
+
TASMOTA_LWT_TOPIC_END,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
from .models import (
|
|
81
|
+
TasmotaDevice,
|
|
82
|
+
TasmotaDeviceConfig,
|
|
83
|
+
TasmotaDeviceSensors,
|
|
84
|
+
TasmotaTimerConfig,
|
|
85
|
+
TasmotaTimezoneConfig,
|
|
86
|
+
TasmotaRule,
|
|
87
|
+
TasmotaTimeZoneDSTSTD,
|
|
88
|
+
)
|
mqttcommander/cli.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from typing import Optional, List, cast, Union
|
|
2
|
+
|
|
3
|
+
from loguru import logger
|
|
4
|
+
|
|
5
|
+
import mqttcommander
|
|
6
|
+
from mqttcommander import (
|
|
7
|
+
MqttCommander,
|
|
8
|
+
TASMOTA_DEFAULT_TOPICS,
|
|
9
|
+
read_tasmotas_from_latest_file,
|
|
10
|
+
TasmotaDevice,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _run(
|
|
15
|
+
host: str,
|
|
16
|
+
port: int,
|
|
17
|
+
username: str,
|
|
18
|
+
password: str,
|
|
19
|
+
action: str,
|
|
20
|
+
command: Optional[str] = None,
|
|
21
|
+
values: Optional[List[str]] = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
mqttcommander.configure_loguru_default_with_skiplog_filter()
|
|
24
|
+
logger.enable("mqttcommander")
|
|
25
|
+
|
|
26
|
+
comm: MqttCommander = MqttCommander(
|
|
27
|
+
topics=TASMOTA_DEFAULT_TOPICS,
|
|
28
|
+
host=host,
|
|
29
|
+
port=port,
|
|
30
|
+
username=username,
|
|
31
|
+
password=password,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
match action:
|
|
35
|
+
case "readfromfile":
|
|
36
|
+
tasmotas = read_tasmotas_from_latest_file(noisy=True)
|
|
37
|
+
count = len(tasmotas) if tasmotas else 0
|
|
38
|
+
print(f"Loaded {count} tasmota devices from latest file")
|
|
39
|
+
case "list-retained":
|
|
40
|
+
msgs = comm.get_all_retained(noisy=False)
|
|
41
|
+
cnt = 0 if msgs is None else len(msgs)
|
|
42
|
+
print(f"Retained messages matching topics {comm.topics}: {cnt}")
|
|
43
|
+
if msgs:
|
|
44
|
+
for m in msgs[:10]:
|
|
45
|
+
print(f"- {m.topic}")
|
|
46
|
+
case "list-tasmotas":
|
|
47
|
+
all_devs = comm.get_all_tasmota_devices_from_retained(noisy=False)
|
|
48
|
+
print(f"Found {len(all_devs)} tasmota devices from retained data")
|
|
49
|
+
for d in all_devs:
|
|
50
|
+
tc = d.tasmota_config
|
|
51
|
+
name = None
|
|
52
|
+
if tc is not None:
|
|
53
|
+
name = tc.device_name or tc.friendly_name or tc.topic
|
|
54
|
+
print(f"- {name}")
|
|
55
|
+
case "list-online":
|
|
56
|
+
all_devs = comm.get_all_tasmota_devices_from_retained(noisy=False)
|
|
57
|
+
online = comm.filter_online_tasmotas_from_retained(all_devs)
|
|
58
|
+
print(f"Online devices: {len(online)} / {len(all_devs)}")
|
|
59
|
+
for d in online:
|
|
60
|
+
tc = d.tasmota_config
|
|
61
|
+
name = None
|
|
62
|
+
if tc is not None:
|
|
63
|
+
name = tc.device_name or tc.friendly_name or tc.topic
|
|
64
|
+
print(f"- {name} online={d.lwt_current_value}")
|
|
65
|
+
case "send-cmd":
|
|
66
|
+
if not command:
|
|
67
|
+
raise SystemExit("--command is required for action send-cmd")
|
|
68
|
+
all_devs = comm.get_all_tasmota_devices_from_retained(noisy=False)
|
|
69
|
+
online = comm.filter_online_tasmotas_from_retained(all_devs)
|
|
70
|
+
vals_typed = cast(List[List[Union[str, float, dict, int]] | None] | None, [values] if values is not None else None)
|
|
71
|
+
comm.send_cmds_to_online_tasmotas(online, to_be_used_commands=[command], values_to_send=vals_typed)
|
|
72
|
+
case _:
|
|
73
|
+
raise SystemExit(f"Unknown action: {action}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def main(argv: Optional[List[str]] = None) -> None:
|
|
77
|
+
import argparse
|
|
78
|
+
|
|
79
|
+
parser = argparse.ArgumentParser(description="mqttcommander CLI")
|
|
80
|
+
|
|
81
|
+
# Common connection options
|
|
82
|
+
parser.add_argument("--host", type=str, required=True, help="MQTT broker host")
|
|
83
|
+
parser.add_argument("--port", type=int, required=True, help="MQTT broker port")
|
|
84
|
+
parser.add_argument("--username", type=str, required=True, help="MQTT username")
|
|
85
|
+
parser.add_argument("--password", type=str, required=True, help="MQTT password")
|
|
86
|
+
|
|
87
|
+
sub = parser.add_subparsers(dest="action", required=True)
|
|
88
|
+
|
|
89
|
+
sub.add_parser("readfromfile", help="Read latest saved tasmota devices file and show count")
|
|
90
|
+
sub.add_parser("list-retained", help="List count of retained messages for default topics")
|
|
91
|
+
sub.add_parser("list-tasmotas", help="Build device list from retained discovery and print devices")
|
|
92
|
+
sub.add_parser("list-online", help="Filter online devices and print them")
|
|
93
|
+
|
|
94
|
+
p_send = sub.add_parser("send-cmd", help="Send a command to all online devices")
|
|
95
|
+
p_send.add_argument("--command", required=True, help="Command, e.g. Power")
|
|
96
|
+
p_send.add_argument("--values", nargs="*", help="Values to send, e.g. Toggle or 1 0 ...")
|
|
97
|
+
|
|
98
|
+
args = parser.parse_args(argv) # argv wird hier verwendet
|
|
99
|
+
|
|
100
|
+
_run(
|
|
101
|
+
host=args.host,
|
|
102
|
+
port=args.port,
|
|
103
|
+
username=args.username,
|
|
104
|
+
password=args.password,
|
|
105
|
+
action=args.action,
|
|
106
|
+
command=getattr(args, "command", None),
|
|
107
|
+
values=getattr(args, "values", None),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
main()
|
mqttcommander/models.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from io import StringIO
|
|
3
|
+
from typing import Optional, Annotated, List, Literal, Any, Self
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field, AliasPath, field_validator, AliasChoices
|
|
6
|
+
from pydantic.networks import IPv4Address
|
|
7
|
+
from pydantic_extra_types.mac_address import MacAddress
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TasmotaTimerConfig(BaseModel):
|
|
11
|
+
# Enable 0 = disarm or disable timer
|
|
12
|
+
# 1 = arm or enable timer
|
|
13
|
+
enable: Optional[Annotated[int, Field(ge=0, le=1)]] = Field(None, validation_alias=AliasChoices("Enable", "enable"))
|
|
14
|
+
|
|
15
|
+
# Mode 0 = use clock time
|
|
16
|
+
# 1 = Use local sunrise time using Longitude, Latitude and Time offset
|
|
17
|
+
# 2 = use local sunset time using Longitude, Latitude and Time offset
|
|
18
|
+
mode: Optional[Annotated[int, Field(ge=0, le=2)]] = Field(None, validation_alias=AliasChoices("Mode", "mode"))
|
|
19
|
+
|
|
20
|
+
# Time When Mode 0 is active
|
|
21
|
+
# > hh:mm = set time in hours 0 .. 23 and minutes 0 .. 59
|
|
22
|
+
# When Mode 1 or Mode 2 is active
|
|
23
|
+
# > +hh:mm or -hh:mm = set offset in hours 0 .. 11 and minutes 0 .. 59 from the time defined by sunrise/sunset.
|
|
24
|
+
time: Optional[str] = Field(None, validation_alias=AliasChoices("Time", "time"))
|
|
25
|
+
|
|
26
|
+
# Window 0..15 = add or subtract a random number of minutes to Time
|
|
27
|
+
window: Optional[Annotated[int, Field(ge=0, le=15)]] = Field(
|
|
28
|
+
None, validation_alias=AliasChoices("Window", "window")
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Days SMTWTFS = set day of weeks mask where 0 or - = OFF and any different character = ON
|
|
32
|
+
days: Optional[Annotated[str, Field(pattern="[1|0|-]{7}")]] = Field(
|
|
33
|
+
None, validation_alias=AliasChoices("Days", "days")
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Repeat 0 = allow timer only once
|
|
37
|
+
# 1 = repeat timer execution
|
|
38
|
+
repeat: Optional[Annotated[int, Field(ge=0, le=1)]] = Field(None, validation_alias=AliasChoices("Repeat", "repeat"))
|
|
39
|
+
|
|
40
|
+
# Output 1..16 = select an output to be used if no rule is enabled
|
|
41
|
+
output: Optional[Annotated[int, Field(ge=1, le=16)]] = Field(
|
|
42
|
+
None, validation_alias=AliasChoices("Output", "output")
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Action 0 = turn output OFF
|
|
46
|
+
# 1 = turn output ON
|
|
47
|
+
# 2 = TOGGLE output
|
|
48
|
+
# 3 = RULE/BLINK
|
|
49
|
+
# If the Tasmota Rules feature has been activated by compiling the code (activated by default in all pre-compiled Tasmota binaries), a rule with Clock#Timer=<timer> will be triggered if written and turned on by the user.
|
|
50
|
+
# If Rules are not compiled, BLINK output using BlinkCount parameters.
|
|
51
|
+
action: Optional[Annotated[int, Field(ge=1, le=16)]] = Field(
|
|
52
|
+
None, validation_alias=AliasChoices("Action", "action")
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# {"Timer1":{"Enable":1,"Mode":0,"Time":"22:00","Window":0,"Days":"1111111","Repeat":1,"Output":1,"Action":0}}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TasmotaTimeZoneDSTSTD(BaseModel):
|
|
59
|
+
hemisphere: Optional[int] = Field(None, validation_alias=AliasChoices("Hemisphere", "hemisphere"))
|
|
60
|
+
week: Optional[int] = Field(None, validation_alias=AliasChoices("Week", "week"))
|
|
61
|
+
month: Optional[int] = Field(None, validation_alias=AliasChoices("Month", "month"))
|
|
62
|
+
day: Optional[int] = Field(None, validation_alias=AliasChoices("Day", "day"))
|
|
63
|
+
hour: Optional[int] = Field(None, validation_alias=AliasChoices("Hour", "hour"))
|
|
64
|
+
offset: Optional[int] = Field(None, validation_alias=AliasChoices("Offset", "offset"))
|
|
65
|
+
|
|
66
|
+
def to_tasmota_command_string(self) -> str:
|
|
67
|
+
return str([self.hemisphere,self.week,self.month,self.day,self.hour,self.offset]).replace(" ", "")
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_tasmota_command_string(cls, values_comma_separated: str) -> TasmotaTimeZoneDSTSTD:
|
|
71
|
+
data: List[str] = values_comma_separated.split(",")
|
|
72
|
+
|
|
73
|
+
assert len(data) == 6
|
|
74
|
+
|
|
75
|
+
return TasmotaTimeZoneDSTSTD(
|
|
76
|
+
hemisphere=int(data[0]),
|
|
77
|
+
week=int(data[1]),
|
|
78
|
+
month=int(data[2]),
|
|
79
|
+
day=int(data[3]),
|
|
80
|
+
hour=int(data[4]),
|
|
81
|
+
offset=int(data[5])
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
class TasmotaTimezoneConfig(BaseModel):
|
|
85
|
+
latitude: Optional[float] = Field(None, validation_alias=AliasChoices("Latitude", "latitude"))
|
|
86
|
+
longitude: Optional[float] = Field(None, validation_alias=AliasChoices("Longitude", "longitude"))
|
|
87
|
+
timedst: Optional[TasmotaTimeZoneDSTSTD] = Field(None, validation_alias=AliasChoices("TimeDst", "timedst"))
|
|
88
|
+
timestd: Optional[TasmotaTimeZoneDSTSTD] = Field(None, validation_alias=AliasChoices("TimeStd", "timestd"))
|
|
89
|
+
timezone: Optional[int | str] = Field(None, validation_alias=AliasChoices("Timezone", "timezone")) # 99 | +01:00
|
|
90
|
+
|
|
91
|
+
def as_tasmota_command_list(self) -> List[str | float | dict[Any, Any] | int] | None:
|
|
92
|
+
assert self.latitude is not None and \
|
|
93
|
+
self.longitude is not None and \
|
|
94
|
+
self.timedst is not None and \
|
|
95
|
+
self.timestd is not None and \
|
|
96
|
+
self.timezone is not None
|
|
97
|
+
|
|
98
|
+
return [
|
|
99
|
+
self.latitude,
|
|
100
|
+
self.longitude,
|
|
101
|
+
self.timedst.to_tasmota_command_string(), # that is correct!
|
|
102
|
+
self.timestd.to_tasmota_command_string(), # that is correct!
|
|
103
|
+
self.timezone
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
def to_tasmota_command_string(self) -> str:
|
|
107
|
+
return str(self.as_tasmota_command_list()).replace(" ", "")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TasmotaDeviceConfig(BaseModel):
|
|
111
|
+
friendly_name: Optional[str] = Field(
|
|
112
|
+
None, validation_alias=AliasChoices("friendly_name", AliasPath("fn", 0))
|
|
113
|
+
) # friendly name -> first element of list of strings?
|
|
114
|
+
device_name: Optional[str] = Field(None, validation_alias=AliasChoices("dn", "device_name"))
|
|
115
|
+
hostname: Optional[str] = Field(None, validation_alias=AliasChoices("hn", "hostname")) # host name
|
|
116
|
+
manufacturer_description: Optional[str] = Field(
|
|
117
|
+
None, validation_alias=AliasChoices("md", "manufacturer_description")
|
|
118
|
+
) # manufacturer description ?!
|
|
119
|
+
ip: Optional[IPv4Address] = None
|
|
120
|
+
mac: Optional[MacAddress] = None
|
|
121
|
+
offline_msg: Optional[str] = Field(None, validation_alias=AliasChoices("ofln", "offline_msg"))
|
|
122
|
+
online_msg: Optional[str] = Field(None, validation_alias=AliasChoices("onln", "online_msg"))
|
|
123
|
+
state: Optional[List[str]] = None
|
|
124
|
+
# t: str = Field(alias="topic", validation_alias='t')
|
|
125
|
+
topic: Optional[str] = Field(None, validation_alias=AliasChoices("t", "topic"))
|
|
126
|
+
tp: Optional[List[str]] = Field(None)
|
|
127
|
+
software_version: Optional[str] = Field(None, validation_alias=AliasChoices("sw", "software_version"))
|
|
128
|
+
timezoneconfig: Optional[TasmotaTimezoneConfig] = None
|
|
129
|
+
teleperiod: Optional[int] = Field(None, validation_alias=AliasChoices("TelePeriod", "teleperiod"))
|
|
130
|
+
powerdelta1: Optional[int] = Field(None, validation_alias=AliasChoices("PowerDelta1", "powerdelta1"))
|
|
131
|
+
setoption4: Optional[Literal["ON", "OFF"]] = Field(None, validation_alias=AliasChoices("SetOption4", "setoption4"))
|
|
132
|
+
timer1: Optional[TasmotaTimerConfig] = Field(None, validation_alias=AliasChoices("Timer1", "timer1"))
|
|
133
|
+
timer2: Optional[TasmotaTimerConfig] = Field(None, validation_alias=AliasChoices("Timer2", "timer2"))
|
|
134
|
+
timer3: Optional[TasmotaTimerConfig] = Field(None, validation_alias=AliasChoices("Timer2", "timer3"))
|
|
135
|
+
timer4: Optional[TasmotaTimerConfig] = Field(None, validation_alias=AliasChoices("Timer4", "timer4"))
|
|
136
|
+
|
|
137
|
+
# SSID1
|
|
138
|
+
# SSID2
|
|
139
|
+
# powerdelta
|
|
140
|
+
# ampres
|
|
141
|
+
# voltres
|
|
142
|
+
# setoption4
|
|
143
|
+
# SetOption53 1; SetOption56 0; SetOption57 0;
|
|
144
|
+
|
|
145
|
+
@field_validator("mac", mode="before")
|
|
146
|
+
@classmethod
|
|
147
|
+
def validate_mac(cls, v: Any) -> Optional[str]:
|
|
148
|
+
# logger.debug(f"VALIDATE MAC: {v}")
|
|
149
|
+
if not v:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
return cls.mac_no_colon_to_colon(v)
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def mac_no_colon_to_colon(mac: str) -> str:
|
|
156
|
+
if mac.find(":") == 2:
|
|
157
|
+
return mac
|
|
158
|
+
|
|
159
|
+
ret: StringIO = StringIO()
|
|
160
|
+
for ch2i in range(0, len(mac), 2):
|
|
161
|
+
if ret.tell() > 0:
|
|
162
|
+
ret.write(":")
|
|
163
|
+
ret.write(mac[ch2i : ch2i + 2])
|
|
164
|
+
|
|
165
|
+
return ret.getvalue()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class TasmotaRule(BaseModel):
|
|
169
|
+
state: Optional[Literal["ON", "OFF"]] = Field(None, validation_alias=AliasChoices("State", "state"))
|
|
170
|
+
once: Optional[Literal["ON", "OFF"]] = Field(None, validation_alias=AliasChoices("Once", "once"))
|
|
171
|
+
stoponerror: Optional[Literal["ON", "OFF"]] = Field(
|
|
172
|
+
None, validation_alias=AliasChoices("StopOnError", "stoponerror")
|
|
173
|
+
)
|
|
174
|
+
length: Optional[int] = Field(None, validation_alias=AliasChoices("Length", "length"))
|
|
175
|
+
rules: Optional[str] = Field(None, validation_alias=AliasChoices("Rules", "rules"))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class TasmotaDeviceSensors(BaseModel):
|
|
179
|
+
# tasmota/discovery/x/sensors
|
|
180
|
+
# sn: dict
|
|
181
|
+
time: datetime = Field(..., validation_alias=AliasChoices("time", "Time", AliasPath("sn", "Time")))
|
|
182
|
+
|
|
183
|
+
# {"sn": {"Time": "2024-07-04T13:09:25", "ANALOG": {"A0": 169},
|
|
184
|
+
# "SHT3X": {"Temperature": 26.1, "Humidity": 44.5, "DewPoint": 13.1}, "TempUnit": "C"}, "ver": 1}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class TasmotaDevice(BaseModel):
|
|
188
|
+
# tasmota/discovery/x/config
|
|
189
|
+
tasmota_config: Optional[TasmotaDeviceConfig] = None
|
|
190
|
+
tasmota_sensors: Optional[TasmotaDeviceSensors] = None
|
|
191
|
+
tasmota_rule1: Optional[TasmotaRule] = None
|
|
192
|
+
tasmota_rule2: Optional[TasmotaRule] = None
|
|
193
|
+
tasmota_rule3: Optional[TasmotaRule] = None
|
|
194
|
+
|
|
195
|
+
lwt_current_value: Optional[Literal["Online", "Offline"]] = None
|
|
196
|
+
|
|
197
|
+
def is_online(self, lwt_online_default_value: str = "Online") -> bool:
|
|
198
|
+
if self.tasmota_config and self.tasmota_config.online_msg:
|
|
199
|
+
return self.lwt_current_value == self.tasmota_config.online_msg
|
|
200
|
+
|
|
201
|
+
return self.lwt_current_value == lwt_online_default_value
|
mqttcommander/py.typed
ADDED
|
File without changes
|