boneIO 0.2.2__tar.gz → 0.3.0.dev1__tar.gz
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.
- {boneio-0.2.2/boneio.egg-info → boneio-0.3.0.dev1}/PKG-INFO +2 -2
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/bonecli.py +19 -12
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/const.py +4 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/helper/__init__.py +19 -1
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/helper/events.py +67 -2
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/helper/exceptions.py +4 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/helper/gpio.py +31 -6
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/helper/loader.py +49 -44
- boneio-0.3.0.dev1/boneio/helper/timeperiod.py +185 -0
- boneio-0.3.0.dev1/boneio/helper/yaml_util.py +305 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/input/gpio.py +5 -1
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/manager.py +13 -6
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/mqtt_client.py +6 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/oled.py +10 -10
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/relay/basic.py +5 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/relay/mcp.py +19 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/schema/actions.yaml +1 -1
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/schema/actions_sensor.yaml +1 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/schema/actions_switch.yaml +1 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/schema/schema.yaml +49 -14
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/sensor/adc.py +10 -1
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/sensor/gpio.py +4 -1
- boneio-0.3.0.dev1/boneio/version.py +2 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1/boneio.egg-info}/PKG-INFO +2 -2
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio.egg-info/SOURCES.txt +2 -1
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio.egg-info/requires.txt +1 -1
- {boneio-0.2.2 → boneio-0.3.0.dev1}/requirements.txt +0 -1
- {boneio-0.2.2 → boneio-0.3.0.dev1}/setup.py +1 -1
- boneio-0.2.2/boneio/helper/yaml.py +0 -64
- boneio-0.2.2/boneio/version.py +0 -2
- {boneio-0.2.2 → boneio-0.3.0.dev1}/LICENSE +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/MANIFEST.in +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/README.md +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/__init__.py +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/cover.py +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/example_config/__init__.py +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/example_config/adc.yaml +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/example_config/config.yaml +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/example_config/cover.yaml +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/example_config/input.yaml +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/example_config/output24x16A.yaml +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/example_config/output32x5A.yaml +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/fonts/danube__.ttf +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/helper/ha_discovery.py +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/helper/logger.py +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/helper/mqtt.py +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/helper/oled.py +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/helper/queue.py +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/helper/state_manager.py +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/helper/stats.py +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/input/__init__.py +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/modbus.py +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/relay/__init__.py +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/relay/gpio.py +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/runner.py +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/sensor/__init__.py +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/sensor/modbus/__init__.py +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/sensor/modbus/sdm120.json +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/sensor/modbus/sdm630.json +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/sensor/modbus/sofar.json +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio/sensor/temp/__init__.py +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio.egg-info/dependency_links.txt +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio.egg-info/entry_points.txt +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/boneio.egg-info/top_level.txt +0 -0
- {boneio-0.2.2 → boneio-0.3.0.dev1}/setup.cfg +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: boneio
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0.dev1
|
|
4
4
|
Summary: Python App for BoneIO
|
|
5
5
|
Home-page: https://github.com/boneIO-eu/app_bbb
|
|
6
|
-
Download-URL: https://github.com/boneIO-eu/app_bbb/archive/0.
|
|
6
|
+
Download-URL: https://github.com/boneIO-eu/app_bbb/archive/0.3.0.dev1.zip
|
|
7
7
|
License: GNU General Public License v3.0
|
|
8
8
|
Platform: UNKNOWN
|
|
9
9
|
Classifier: Programming Language :: Python
|
|
@@ -8,9 +8,11 @@ from colorlog import ColoredFormatter
|
|
|
8
8
|
|
|
9
9
|
from boneio.const import ACTION
|
|
10
10
|
from boneio.helper import load_config_from_file
|
|
11
|
+
from boneio.helper.exceptions import ConfigurationException
|
|
11
12
|
from boneio.helper.logger import configure_logger
|
|
12
13
|
from boneio.runner import async_run
|
|
13
14
|
from boneio.version import __version__
|
|
15
|
+
from yaml import MarkedYAMLError
|
|
14
16
|
|
|
15
17
|
TASK_CANCELATION_TIMEOUT = 1
|
|
16
18
|
|
|
@@ -67,18 +69,23 @@ def get_arguments() -> argparse.Namespace:
|
|
|
67
69
|
def run(config: str, debug: int, mqttusername: str = "", mqttpassword: str = ""):
|
|
68
70
|
"""Run BoneIO."""
|
|
69
71
|
_LOGGER.info("BoneIO starting.")
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
72
|
+
try:
|
|
73
|
+
_config = load_config_from_file(config_file=config)
|
|
74
|
+
if not _config:
|
|
75
|
+
_LOGGER.error("Config not loaded. Exiting.")
|
|
76
|
+
return 1
|
|
77
|
+
configure_logger(log_config=_config.get("logger"), debug=debug)
|
|
78
|
+
return asyncio.run(
|
|
79
|
+
async_run(
|
|
80
|
+
config=_config,
|
|
81
|
+
config_file=config,
|
|
82
|
+
mqttusername=mqttusername,
|
|
83
|
+
mqttpassword=mqttpassword,
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
except (ConfigurationException, MarkedYAMLError) as err:
|
|
87
|
+
_LOGGER.error("Failed to load config. %s Exiting.", err)
|
|
88
|
+
return 1
|
|
82
89
|
|
|
83
90
|
|
|
84
91
|
def main() -> int:
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
"""Helper dir for BoneIO."""
|
|
2
|
+
from typing import Callable, TypeVar, Any
|
|
3
|
+
|
|
2
4
|
from boneio.helper.gpio import (
|
|
3
5
|
edge_detect,
|
|
4
6
|
setup_input,
|
|
@@ -11,7 +13,7 @@ from boneio.helper.gpio import (
|
|
|
11
13
|
from boneio.helper.oled import make_font
|
|
12
14
|
from boneio.helper.mqtt import BasicMqtt
|
|
13
15
|
from boneio.helper.stats import HostData, host_stats
|
|
14
|
-
from boneio.helper.
|
|
16
|
+
from boneio.helper.yaml_util import (
|
|
15
17
|
CustomValidator,
|
|
16
18
|
load_yaml_file,
|
|
17
19
|
schema_file,
|
|
@@ -31,6 +33,21 @@ from boneio.helper.ha_discovery import (
|
|
|
31
33
|
)
|
|
32
34
|
from boneio.helper.exceptions import GPIOInputException, I2CError
|
|
33
35
|
from boneio.helper.queue import UniqueQueue
|
|
36
|
+
from boneio.helper.timeperiod import TimePeriod
|
|
37
|
+
|
|
38
|
+
CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable[..., Any])
|
|
39
|
+
CALLBACK_TYPE = Callable[[], None]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def callback(func: CALLABLE_T) -> CALLABLE_T:
|
|
43
|
+
"""Annotation to mark method as safe to call from within the event loop."""
|
|
44
|
+
setattr(func, "_boneio_callback", True)
|
|
45
|
+
return func
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_callback(func: Callable[..., Any]) -> bool:
|
|
49
|
+
"""Check if function is safe to be called in the event loop."""
|
|
50
|
+
return getattr(func, "_boneio_callback", False) is True
|
|
34
51
|
|
|
35
52
|
|
|
36
53
|
__all__ = [
|
|
@@ -62,4 +79,5 @@ __all__ = [
|
|
|
62
79
|
"schema_file",
|
|
63
80
|
"load_config_from_string",
|
|
64
81
|
"load_config_from_file",
|
|
82
|
+
"TimePeriod",
|
|
65
83
|
]
|
|
@@ -2,16 +2,24 @@ import asyncio
|
|
|
2
2
|
import datetime as dt
|
|
3
3
|
import signal
|
|
4
4
|
import logging
|
|
5
|
+
import time
|
|
6
|
+
from typing import Callable
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from boneio.helper import callback
|
|
5
9
|
|
|
6
10
|
_LOGGER = logging.getLogger(__name__)
|
|
7
11
|
UTC = dt.timezone.utc
|
|
8
12
|
EVENT_TIME_CHANGED = "event_time_changed"
|
|
13
|
+
CALLBACK_TYPE = Callable[[], None]
|
|
9
14
|
|
|
10
15
|
|
|
11
|
-
def utcnow():
|
|
16
|
+
def utcnow() -> dt.datetime:
|
|
12
17
|
return dt.datetime.now(UTC)
|
|
13
18
|
|
|
14
19
|
|
|
20
|
+
time_tracker_utcnow = utcnow
|
|
21
|
+
|
|
22
|
+
|
|
15
23
|
def _async_create_timer(loop: asyncio.AbstractEventLoop, event_callback) -> None:
|
|
16
24
|
"""Create a timer that will start on BoneIO start."""
|
|
17
25
|
handle = None
|
|
@@ -19,7 +27,7 @@ def _async_create_timer(loop: asyncio.AbstractEventLoop, event_callback) -> None
|
|
|
19
27
|
def schedule_tick(now: dt.datetime) -> None:
|
|
20
28
|
"""Schedule a timer tick when the next second rolls around."""
|
|
21
29
|
nonlocal handle
|
|
22
|
-
slp_seconds = 1 - (now.microsecond / 10
|
|
30
|
+
slp_seconds = 1 - (now.microsecond / 10**6)
|
|
23
31
|
handle = loop.call_later(slp_seconds, fire_time_event)
|
|
24
32
|
|
|
25
33
|
def fire_time_event() -> None:
|
|
@@ -109,3 +117,60 @@ class EventBus:
|
|
|
109
117
|
"""Remove regular listener."""
|
|
110
118
|
if name in self._listeners:
|
|
111
119
|
del self._listeners[name]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def as_utc(dattim: dt.datetime) -> dt.datetime:
|
|
123
|
+
"""Return a datetime as UTC time.
|
|
124
|
+
|
|
125
|
+
Assumes datetime without tzinfo to be in the DEFAULT_TIME_ZONE.
|
|
126
|
+
"""
|
|
127
|
+
if dattim.tzinfo == UTC:
|
|
128
|
+
return dattim
|
|
129
|
+
return dattim.astimezone(UTC)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@callback
|
|
133
|
+
def async_track_point_in_time(
|
|
134
|
+
loop: asyncio.AbstractEventLoop,
|
|
135
|
+
action,
|
|
136
|
+
point_in_time: datetime,
|
|
137
|
+
) -> CALLBACK_TYPE:
|
|
138
|
+
"""Add a listener that fires once after a specific point in UTC time."""
|
|
139
|
+
# Ensure point_in_time is UTC
|
|
140
|
+
utc_point_in_time = as_utc(point_in_time)
|
|
141
|
+
|
|
142
|
+
# Since this is called once, we accept a HassJob so we can avoid
|
|
143
|
+
# having to figure out how to call the action every time its called.
|
|
144
|
+
cancel_callback: asyncio.TimerHandle | None = None
|
|
145
|
+
|
|
146
|
+
@callback
|
|
147
|
+
def run_action(job) -> None:
|
|
148
|
+
"""Call the action."""
|
|
149
|
+
nonlocal cancel_callback
|
|
150
|
+
|
|
151
|
+
now = time_tracker_utcnow()
|
|
152
|
+
|
|
153
|
+
# Depending on the available clock support (including timer hardware
|
|
154
|
+
# and the OS kernel) it can happen that we fire a little bit too early
|
|
155
|
+
# as measured by utcnow(). That is bad when callbacks have assumptions
|
|
156
|
+
# about the current time. Thus, we rearm the timer for the remaining
|
|
157
|
+
# time.
|
|
158
|
+
delta = (utc_point_in_time - now).total_seconds()
|
|
159
|
+
if delta > 0:
|
|
160
|
+
_LOGGER.debug("Called %f seconds too early, rearming", delta)
|
|
161
|
+
|
|
162
|
+
cancel_callback = loop.call_later(delta, run_action, job)
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
loop.call_soon(job, utc_point_in_time)
|
|
166
|
+
|
|
167
|
+
delta = utc_point_in_time.timestamp() - time.time()
|
|
168
|
+
cancel_callback = loop.call_later(delta, run_action, action)
|
|
169
|
+
|
|
170
|
+
@callback
|
|
171
|
+
def unsub_point_in_time_listener() -> None:
|
|
172
|
+
"""Cancel the call_later."""
|
|
173
|
+
assert cancel_callback is not None
|
|
174
|
+
cancel_callback.cancel()
|
|
175
|
+
|
|
176
|
+
return unsub_point_in_time_listener
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from Adafruit_BBIO import GPIO
|
|
6
|
+
except ModuleNotFoundError:
|
|
7
|
+
|
|
8
|
+
class GPIO:
|
|
9
|
+
PUD_OFF = "poff"
|
|
10
|
+
PUD_UP = "poff"
|
|
11
|
+
PUD_DOWN = "poff"
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
pass
|
|
4
17
|
|
|
5
18
|
from boneio.helper.exceptions import GPIOInputException
|
|
6
19
|
from boneio.const import LOW, FALLING, Gpio_Edges, Gpio_States
|
|
20
|
+
from boneio.helper.timeperiod import TimePeriod
|
|
21
|
+
|
|
7
22
|
from typing import Callable
|
|
8
23
|
import subprocess
|
|
9
24
|
|
|
@@ -34,10 +49,19 @@ def setup_output(pin: str) -> None:
|
|
|
34
49
|
GPIO.setup(pin, GPIO.OUT, pull_up_down=GPIO.PUD_DOWN)
|
|
35
50
|
|
|
36
51
|
|
|
37
|
-
|
|
52
|
+
gpio_modes = {
|
|
53
|
+
"gpio": GPIO.PUD_OFF,
|
|
54
|
+
"gpio_pu": GPIO.PUD_UP,
|
|
55
|
+
"gpio_pd": GPIO.PUD_DOWN,
|
|
56
|
+
"gpio_input": GPIO.PUD_OFF,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def setup_input(pin: str, pull_mode: str = "gpio") -> None:
|
|
38
61
|
"""Set up a GPIO as input."""
|
|
62
|
+
gpio_mode = gpio_modes.get(pull_mode, GPIO.PUD_OFF)
|
|
39
63
|
try:
|
|
40
|
-
GPIO.setup(pin, GPIO.IN,
|
|
64
|
+
GPIO.setup(pin, GPIO.IN, gpio_mode)
|
|
41
65
|
except (ValueError, SystemError) as err:
|
|
42
66
|
raise GPIOInputException(err)
|
|
43
67
|
|
|
@@ -72,11 +96,12 @@ class GpioBaseClass:
|
|
|
72
96
|
"""Setup GPIO Input Button"""
|
|
73
97
|
self._pin = pin
|
|
74
98
|
gpio = kwargs.get("rest_pin", {GPIO_MODE: GPIO_STR})
|
|
75
|
-
|
|
76
|
-
|
|
99
|
+
gpio_mode = gpio.get(GPIO_MODE, GPIO_STR)
|
|
100
|
+
self._bounce_time = gpio.get("bounce_time", TimePeriod(milliseconds=25))
|
|
101
|
+
configure_pin(pin=pin, mode=gpio_mode)
|
|
77
102
|
self._loop = asyncio.get_running_loop()
|
|
78
103
|
self._press_callback = press_callback
|
|
79
|
-
setup_input(pin=self._pin)
|
|
104
|
+
setup_input(pin=self._pin, pull_mode=gpio_mode)
|
|
80
105
|
|
|
81
106
|
@property
|
|
82
107
|
def is_pressed(self):
|
|
@@ -192,6 +192,17 @@ def create_modbus_sensors(
|
|
|
192
192
|
pass
|
|
193
193
|
|
|
194
194
|
|
|
195
|
+
OutputEntry = namedtuple("OutputEntry", "OutputClass output_kind output_id")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def output_chooser(output_kind: str, mcp_id: str | None):
|
|
199
|
+
"""Get named tuple based on input."""
|
|
200
|
+
if output_kind == MCP:
|
|
201
|
+
return OutputEntry(MCPRelay, MCP, mcp_id)
|
|
202
|
+
else:
|
|
203
|
+
return OutputEntry(GpioRelay, GPIO, GPIO)
|
|
204
|
+
|
|
205
|
+
|
|
195
206
|
def configure_relay(
|
|
196
207
|
manager: Manager,
|
|
197
208
|
state_manager: StateManager,
|
|
@@ -201,61 +212,55 @@ def configure_relay(
|
|
|
201
212
|
config: dict,
|
|
202
213
|
) -> Any:
|
|
203
214
|
"""Configure kind of relay. Most common MCP."""
|
|
215
|
+
restore_state = config.pop(RESTORE_STATE, False)
|
|
216
|
+
output_type = config.pop(OUTPUT_TYPE)
|
|
204
217
|
restored_state = (
|
|
205
218
|
state_manager.get(attr_type=RELAY, attr=relay_id, default_value=False)
|
|
206
|
-
if
|
|
219
|
+
if restore_state
|
|
207
220
|
else False
|
|
208
221
|
)
|
|
209
|
-
if
|
|
210
|
-
attr_type=RELAY, attr=relay_id
|
|
211
|
-
):
|
|
222
|
+
if output_type == NONE and state_manager.get(attr_type=RELAY, attr=relay_id):
|
|
212
223
|
state_manager.del_attribute(attr_type=RELAY, attribute=relay_id)
|
|
213
224
|
restored_state = False
|
|
214
225
|
|
|
215
|
-
|
|
216
|
-
mcp_id
|
|
217
|
-
|
|
226
|
+
output = output_chooser(
|
|
227
|
+
output_kind=config.pop(KIND), mcp_id=config.pop(MCP_ID, None)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if getattr(output, "output_kind") == MCP:
|
|
231
|
+
mcp = manager.mcp.get(getattr(output, "output_id"))
|
|
218
232
|
if not mcp:
|
|
219
233
|
_LOGGER.error("No such MCP configured!")
|
|
220
|
-
return
|
|
221
|
-
|
|
222
|
-
pin
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
output_type=config[OUTPUT_TYPE].lower(),
|
|
229
|
-
restored_state=restored_state,
|
|
230
|
-
callback=lambda: relay_callback(
|
|
231
|
-
relay_type=mcp_id,
|
|
232
|
-
relay_id=relay_id,
|
|
233
|
-
restore_state=False
|
|
234
|
-
if config[OUTPUT_TYPE] == NONE
|
|
235
|
-
else config[RESTORE_STATE],
|
|
236
|
-
),
|
|
237
|
-
)
|
|
238
|
-
manager.grouped_outputs[mcp_id][relay_id] = mcp_relay
|
|
239
|
-
return mcp_relay
|
|
240
|
-
elif config[KIND] == GPIO:
|
|
234
|
+
return None
|
|
235
|
+
kwargs = {
|
|
236
|
+
"pin": int(config.pop(PIN)),
|
|
237
|
+
"mcp": mcp,
|
|
238
|
+
"mcp_id": getattr(output, "output_id"),
|
|
239
|
+
"output_type": output_type,
|
|
240
|
+
}
|
|
241
|
+
elif getattr(output, "output_kind") == GPIO:
|
|
241
242
|
if GPIO not in manager.grouped_outputs:
|
|
242
243
|
manager.grouped_outputs[GPIO] = {}
|
|
243
|
-
|
|
244
|
-
pin
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
244
|
+
kwargs = {
|
|
245
|
+
"pin": config.pop(PIN),
|
|
246
|
+
}
|
|
247
|
+
else:
|
|
248
|
+
return
|
|
249
|
+
relay = getattr(output, "OutputClass")(
|
|
250
|
+
send_message=manager.send_message,
|
|
251
|
+
topic_prefix=topic_prefix,
|
|
252
|
+
id=config.pop(ID),
|
|
253
|
+
restored_state=restored_state,
|
|
254
|
+
**config,
|
|
255
|
+
**kwargs,
|
|
256
|
+
callback=lambda: relay_callback(
|
|
257
|
+
relay_type=getattr(output, "output_id"),
|
|
258
|
+
relay_id=relay_id,
|
|
259
|
+
restore_state=False if output_type == NONE else restore_state,
|
|
260
|
+
),
|
|
261
|
+
)
|
|
262
|
+
manager.grouped_outputs[getattr(output, "output_id")][relay_id] = relay
|
|
263
|
+
return relay
|
|
259
264
|
|
|
260
265
|
|
|
261
266
|
InputEntry = namedtuple(
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from collections import OrderedDict
|
|
2
|
+
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def is_approximately_integer(value):
|
|
8
|
+
if isinstance(value, int):
|
|
9
|
+
return True
|
|
10
|
+
return abs(value - round(value)) < 0.001
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TimePeriod:
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
microseconds=None,
|
|
17
|
+
milliseconds=None,
|
|
18
|
+
seconds=None,
|
|
19
|
+
minutes=None,
|
|
20
|
+
hours=None,
|
|
21
|
+
days=None,
|
|
22
|
+
) -> None:
|
|
23
|
+
if days is not None:
|
|
24
|
+
if not is_approximately_integer(days):
|
|
25
|
+
frac_days, days = math.modf(days)
|
|
26
|
+
hours = (hours or 0) + frac_days * 24
|
|
27
|
+
self.days = int(round(days))
|
|
28
|
+
else:
|
|
29
|
+
self.days = None
|
|
30
|
+
|
|
31
|
+
if hours is not None:
|
|
32
|
+
if not is_approximately_integer(hours):
|
|
33
|
+
frac_hours, hours = math.modf(hours)
|
|
34
|
+
minutes = (minutes or 0) + frac_hours * 60
|
|
35
|
+
self.hours = int(round(hours))
|
|
36
|
+
else:
|
|
37
|
+
self.hours = None
|
|
38
|
+
|
|
39
|
+
if minutes is not None:
|
|
40
|
+
if not is_approximately_integer(minutes):
|
|
41
|
+
frac_minutes, minutes = math.modf(minutes)
|
|
42
|
+
seconds = (seconds or 0) + frac_minutes * 60
|
|
43
|
+
self.minutes = int(round(minutes))
|
|
44
|
+
else:
|
|
45
|
+
self.minutes = None
|
|
46
|
+
|
|
47
|
+
if seconds is not None:
|
|
48
|
+
if not is_approximately_integer(seconds):
|
|
49
|
+
frac_seconds, seconds = math.modf(seconds)
|
|
50
|
+
milliseconds = (milliseconds or 0) + frac_seconds * 1000
|
|
51
|
+
self.seconds = int(round(seconds))
|
|
52
|
+
else:
|
|
53
|
+
self.seconds = None
|
|
54
|
+
|
|
55
|
+
if milliseconds is not None:
|
|
56
|
+
if not is_approximately_integer(milliseconds):
|
|
57
|
+
frac_milliseconds, milliseconds = math.modf(milliseconds)
|
|
58
|
+
microseconds = (microseconds or 0) + frac_milliseconds * 1000
|
|
59
|
+
self.milliseconds = int(round(milliseconds))
|
|
60
|
+
else:
|
|
61
|
+
self.milliseconds = None
|
|
62
|
+
|
|
63
|
+
if microseconds is not None:
|
|
64
|
+
if not is_approximately_integer(microseconds):
|
|
65
|
+
raise ValueError("Maximum precision is microseconds")
|
|
66
|
+
self.microseconds = int(round(microseconds))
|
|
67
|
+
else:
|
|
68
|
+
self.microseconds = None
|
|
69
|
+
|
|
70
|
+
self._timedelta = timedelta(
|
|
71
|
+
days=self.days or 0,
|
|
72
|
+
hours=self.hours or 0,
|
|
73
|
+
minutes=self.minutes or 0,
|
|
74
|
+
seconds=self.seconds or 0,
|
|
75
|
+
milliseconds=self.milliseconds or 0,
|
|
76
|
+
microseconds=self.microseconds or 0,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def as_timedelta(self) -> timedelta:
|
|
81
|
+
return self._timedelta
|
|
82
|
+
|
|
83
|
+
def as_dict(self) -> OrderedDict:
|
|
84
|
+
out = OrderedDict()
|
|
85
|
+
if self.microseconds is not None:
|
|
86
|
+
out["microseconds"] = self.microseconds
|
|
87
|
+
if self.milliseconds is not None:
|
|
88
|
+
out["milliseconds"] = self.milliseconds
|
|
89
|
+
if self.seconds is not None:
|
|
90
|
+
out["seconds"] = self.seconds
|
|
91
|
+
if self.minutes is not None:
|
|
92
|
+
out["minutes"] = self.minutes
|
|
93
|
+
if self.hours is not None:
|
|
94
|
+
out["hours"] = self.hours
|
|
95
|
+
if self.days is not None:
|
|
96
|
+
out["days"] = self.days
|
|
97
|
+
return out
|
|
98
|
+
|
|
99
|
+
def __str__(self) -> str:
|
|
100
|
+
if self.microseconds is not None:
|
|
101
|
+
return f"{self.total_microseconds}us"
|
|
102
|
+
if self.milliseconds is not None:
|
|
103
|
+
return f"{self.total_milliseconds}ms"
|
|
104
|
+
if self.seconds is not None:
|
|
105
|
+
return f"{self.total_seconds}s"
|
|
106
|
+
if self.minutes is not None:
|
|
107
|
+
return f"{self.total_minutes}min"
|
|
108
|
+
if self.hours is not None:
|
|
109
|
+
return f"{self.total_hours}h"
|
|
110
|
+
if self.days is not None:
|
|
111
|
+
return f"{self.total_days}d"
|
|
112
|
+
return "0s"
|
|
113
|
+
|
|
114
|
+
def __repr__(self):
|
|
115
|
+
return f"TimePeriod<{self.total_microseconds}>"
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def total_microseconds(self):
|
|
119
|
+
return self.total_milliseconds * 1000 + (self.microseconds or 0)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def total_milliseconds(self):
|
|
123
|
+
return self.total_seconds * 1000 + (self.milliseconds or 0)
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def total_seconds(self):
|
|
127
|
+
return self.total_minutes * 60 + (self.seconds or 0)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def total_minutes(self):
|
|
131
|
+
return self.total_hours * 60 + (self.minutes or 0)
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def total_hours(self):
|
|
135
|
+
return self.total_days * 24 + (self.hours or 0)
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def total_days(self):
|
|
139
|
+
return self.days or 0
|
|
140
|
+
|
|
141
|
+
def __eq__(self, other):
|
|
142
|
+
if isinstance(other, TimePeriod):
|
|
143
|
+
return self.total_microseconds == other.total_microseconds
|
|
144
|
+
return NotImplemented
|
|
145
|
+
|
|
146
|
+
def __ne__(self, other):
|
|
147
|
+
if isinstance(other, TimePeriod):
|
|
148
|
+
return self.total_microseconds != other.total_microseconds
|
|
149
|
+
return NotImplemented
|
|
150
|
+
|
|
151
|
+
def __lt__(self, other):
|
|
152
|
+
if isinstance(other, TimePeriod):
|
|
153
|
+
return self.total_microseconds < other.total_microseconds
|
|
154
|
+
return NotImplemented
|
|
155
|
+
|
|
156
|
+
def __gt__(self, other):
|
|
157
|
+
if isinstance(other, TimePeriod):
|
|
158
|
+
return self.total_microseconds > other.total_microseconds
|
|
159
|
+
return NotImplemented
|
|
160
|
+
|
|
161
|
+
def __le__(self, other):
|
|
162
|
+
if isinstance(other, TimePeriod):
|
|
163
|
+
return self.total_microseconds <= other.total_microseconds
|
|
164
|
+
return NotImplemented
|
|
165
|
+
|
|
166
|
+
def __ge__(self, other):
|
|
167
|
+
if isinstance(other, TimePeriod):
|
|
168
|
+
return self.total_microseconds >= other.total_microseconds
|
|
169
|
+
return NotImplemented
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TimePeriodMicroseconds(TimePeriod):
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class TimePeriodMilliseconds(TimePeriod):
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class TimePeriodSeconds(TimePeriod):
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class TimePeriodMinutes(TimePeriod):
|
|
185
|
+
pass
|