boneIO 0.1.0__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.
- boneio/__init__.py +11 -0
- boneio/bonecli.py +165 -0
- boneio/const.py +111 -0
- boneio/example_config/__init__.py +0 -0
- boneio/example_config/adc.yaml +14 -0
- boneio/example_config/config.yaml +25 -0
- boneio/example_config/input.yaml +200 -0
- boneio/example_config/output24x16A.yaml +139 -0
- boneio/example_config/output32x5A.yaml +160 -0
- boneio/fonts/danube__.ttf +0 -0
- boneio/helper/__init__.py +50 -0
- boneio/helper/exceptions.py +13 -0
- boneio/helper/gpio.py +81 -0
- boneio/helper/ha_discovery.py +128 -0
- boneio/helper/mqtt.py +30 -0
- boneio/helper/oled.py +15 -0
- boneio/helper/queue.py +32 -0
- boneio/helper/stats.py +185 -0
- boneio/helper/yaml.py +46 -0
- boneio/input/__init__.py +5 -0
- boneio/input/gpio.py +122 -0
- boneio/manager.py +398 -0
- boneio/modbus.py +107 -0
- boneio/mqtt_client.py +170 -0
- boneio/oled.py +118 -0
- boneio/relay/__init__.py +5 -0
- boneio/relay/basic.py +76 -0
- boneio/relay/gpio.py +44 -0
- boneio/relay/mcp.py +56 -0
- boneio/schema.yaml +310 -0
- boneio/sensor/__init__.py +12 -0
- boneio/sensor/adc.py +38 -0
- boneio/sensor/gpio.py +32 -0
- boneio/sensor/sdm630.py +193 -0
- boneio/sensor/temp/__init__.py +52 -0
- boneio/version.py +2 -0
- boneio-0.1.0.dist-info/LICENSE +674 -0
- boneio-0.1.0.dist-info/METADATA +51 -0
- boneio-0.1.0.dist-info/RECORD +42 -0
- boneio-0.1.0.dist-info/WHEEL +5 -0
- boneio-0.1.0.dist-info/entry_points.txt +3 -0
- boneio-0.1.0.dist-info/top_level.txt +1 -0
boneio/helper/yaml.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
import os
|
|
3
|
+
from cerberus import Validator
|
|
4
|
+
from boneio.const import ID, OUTPUT
|
|
5
|
+
from yaml import load, YAMLError, SafeLoader
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Loader(SafeLoader):
|
|
9
|
+
"""Loader which support for include in yaml files."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, stream):
|
|
12
|
+
|
|
13
|
+
self._root = os.path.split(stream.name)[0]
|
|
14
|
+
|
|
15
|
+
super(Loader, self).__init__(stream)
|
|
16
|
+
|
|
17
|
+
def include(self, node):
|
|
18
|
+
|
|
19
|
+
filename = os.path.join(self._root, self.construct_scalar(node))
|
|
20
|
+
|
|
21
|
+
with open(filename, "r") as f:
|
|
22
|
+
return load(f, Loader)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
Loader.add_constructor("!include", Loader.include)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_yaml_file(filename: str) -> Any:
|
|
29
|
+
with open(filename, "r") as stream:
|
|
30
|
+
try:
|
|
31
|
+
return load(stream, Loader=Loader)
|
|
32
|
+
except YAMLError as exception:
|
|
33
|
+
raise exception
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CustomValidator(Validator):
|
|
37
|
+
"""Custom validator of cerberus"""
|
|
38
|
+
|
|
39
|
+
def _check_with_output_id_uniqueness(self, field, value):
|
|
40
|
+
"""Check if outputs ids are unique."""
|
|
41
|
+
all_ids = [x[ID] for x in self.document[OUTPUT]]
|
|
42
|
+
if len(all_ids) != len(set(all_ids)):
|
|
43
|
+
self._error(field, "Output IDs are not unique.")
|
|
44
|
+
|
|
45
|
+
def _normalize_coerce_to_bool(self, value):
|
|
46
|
+
return True
|
boneio/input/__init__.py
ADDED
boneio/input/gpio.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""GPIOInputButton to receive signals."""
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from functools import partial
|
|
5
|
+
|
|
6
|
+
from boneio.const import (
|
|
7
|
+
DEBOUNCE_DURATION,
|
|
8
|
+
DELAY_DURATION,
|
|
9
|
+
DOUBLE,
|
|
10
|
+
LONG,
|
|
11
|
+
LONG_PRESS_DURATION,
|
|
12
|
+
SECOND_DELAY_DURATION,
|
|
13
|
+
SINGLE,
|
|
14
|
+
)
|
|
15
|
+
from boneio.helper import edge_detect, GpioBaseClass
|
|
16
|
+
|
|
17
|
+
_LOGGER = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GpioInputButton(GpioBaseClass):
|
|
21
|
+
"""Represent Gpio input switch."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, **kwargs) -> None:
|
|
24
|
+
"""Setup GPIO Input Button"""
|
|
25
|
+
super().__init__(**kwargs)
|
|
26
|
+
edge_detect(self._pin, callback=self._handle_press, bounce=25)
|
|
27
|
+
self._first_press_timestamp = None
|
|
28
|
+
self._is_long_press = False
|
|
29
|
+
self._second_press_timestamp = None
|
|
30
|
+
self._second_check = False
|
|
31
|
+
_LOGGER.debug("Configured listening for input pin %s", self._pin)
|
|
32
|
+
|
|
33
|
+
def _handle_press(self, pin: str) -> None:
|
|
34
|
+
"""Handle the button press callback"""
|
|
35
|
+
# Ignore if we are in a long press
|
|
36
|
+
if self._is_long_press:
|
|
37
|
+
return
|
|
38
|
+
now = datetime.now()
|
|
39
|
+
|
|
40
|
+
# Debounce button
|
|
41
|
+
if (
|
|
42
|
+
self._first_press_timestamp is not None
|
|
43
|
+
and now - self._first_press_timestamp < DEBOUNCE_DURATION
|
|
44
|
+
):
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
# Second click debounce. Just in case.
|
|
48
|
+
if (
|
|
49
|
+
self._second_press_timestamp is not None
|
|
50
|
+
and now - self._second_press_timestamp < DEBOUNCE_DURATION
|
|
51
|
+
):
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
if not self._first_press_timestamp:
|
|
55
|
+
self._first_press_timestamp = now
|
|
56
|
+
elif not self._second_press_timestamp:
|
|
57
|
+
self._second_press_timestamp = now
|
|
58
|
+
|
|
59
|
+
self._loop.call_soon_threadsafe(
|
|
60
|
+
self._loop.call_later,
|
|
61
|
+
DELAY_DURATION,
|
|
62
|
+
self.check_press_length,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def check_press_length(self) -> None:
|
|
66
|
+
"""Check if it's a single, double or long press"""
|
|
67
|
+
# Check if button is still pressed
|
|
68
|
+
if self.is_pressed:
|
|
69
|
+
# Schedule a new check
|
|
70
|
+
self._loop.call_soon_threadsafe(
|
|
71
|
+
self._loop.call_later,
|
|
72
|
+
DELAY_DURATION,
|
|
73
|
+
self.check_press_length,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Handle edge case due to multiple clicks
|
|
77
|
+
if self._first_press_timestamp is None:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
# Check if we reached a long press
|
|
81
|
+
diff = datetime.now() - self._first_press_timestamp
|
|
82
|
+
if not self._is_long_press and diff > LONG_PRESS_DURATION:
|
|
83
|
+
self._is_long_press = True
|
|
84
|
+
_LOGGER.debug("Long button press on pin %s, call callback", self._pin)
|
|
85
|
+
self._loop.call_soon_threadsafe(
|
|
86
|
+
partial(self._press_callback, LONG, self._pin)
|
|
87
|
+
)
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# Handle short press
|
|
91
|
+
if not self._is_long_press:
|
|
92
|
+
if not self._second_press_timestamp and not self._second_check:
|
|
93
|
+
# let's try to check if second click will atempt
|
|
94
|
+
self._second_check = True
|
|
95
|
+
self._loop.call_soon_threadsafe(
|
|
96
|
+
self._loop.call_later,
|
|
97
|
+
SECOND_DELAY_DURATION,
|
|
98
|
+
self.check_press_length,
|
|
99
|
+
)
|
|
100
|
+
return
|
|
101
|
+
if self._second_check:
|
|
102
|
+
if self._second_press_timestamp:
|
|
103
|
+
_LOGGER.debug(
|
|
104
|
+
"Double click event on pin %s, roznica %s",
|
|
105
|
+
self._pin,
|
|
106
|
+
self._second_press_timestamp - self._first_press_timestamp,
|
|
107
|
+
)
|
|
108
|
+
self._loop.call_soon_threadsafe(
|
|
109
|
+
partial(self._press_callback, DOUBLE, self._pin)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
elif self._first_press_timestamp:
|
|
113
|
+
_LOGGER.debug("One click event on pin %s, call callback", self._pin)
|
|
114
|
+
self._loop.call_soon_threadsafe(
|
|
115
|
+
partial(self._press_callback, SINGLE, self._pin)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Clean state on button released
|
|
119
|
+
self._first_press_timestamp = None
|
|
120
|
+
self._second_press_timestamp = None
|
|
121
|
+
self._second_check = False
|
|
122
|
+
self._is_long_press = False
|
boneio/manager.py
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from boneio.modbus import Modbus
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Callable, List, Optional, Set, Union
|
|
6
|
+
|
|
7
|
+
from adafruit_mcp230xx.mcp23017 import MCP23017
|
|
8
|
+
from board import SCL, SDA
|
|
9
|
+
from busio import I2C
|
|
10
|
+
|
|
11
|
+
from boneio.const import (
|
|
12
|
+
ACTION,
|
|
13
|
+
ACTIONS,
|
|
14
|
+
ADDRESS,
|
|
15
|
+
GPIO,
|
|
16
|
+
HA_TYPE,
|
|
17
|
+
HOMEASSISTANT,
|
|
18
|
+
ID,
|
|
19
|
+
INIT_SLEEP,
|
|
20
|
+
INPUT,
|
|
21
|
+
KIND,
|
|
22
|
+
LM75,
|
|
23
|
+
MCP,
|
|
24
|
+
MCP_ID,
|
|
25
|
+
MCP_TEMP_9808,
|
|
26
|
+
OFF,
|
|
27
|
+
ON,
|
|
28
|
+
ONLINE,
|
|
29
|
+
OUTPUT,
|
|
30
|
+
PIN,
|
|
31
|
+
RELAY,
|
|
32
|
+
SDM630,
|
|
33
|
+
STATE,
|
|
34
|
+
ClickTypes,
|
|
35
|
+
InputTypes,
|
|
36
|
+
SENSOR,
|
|
37
|
+
SHOW_HA,
|
|
38
|
+
BINARY_SENSOR,
|
|
39
|
+
INPUT_SENSOR,
|
|
40
|
+
UART,
|
|
41
|
+
UPDATE_INTERVAL,
|
|
42
|
+
UARTS,
|
|
43
|
+
)
|
|
44
|
+
from boneio.helper import (
|
|
45
|
+
HostData,
|
|
46
|
+
ha_relay_availabilty_message,
|
|
47
|
+
ha_input_availabilty_message,
|
|
48
|
+
ha_adc_sensor_availabilty_message,
|
|
49
|
+
ha_sensor_temp_availabilty_message,
|
|
50
|
+
ha_binary_sensor_availabilty_message,
|
|
51
|
+
host_stats,
|
|
52
|
+
GPIOInputException,
|
|
53
|
+
I2CError,
|
|
54
|
+
)
|
|
55
|
+
from boneio.input import GpioInputButton
|
|
56
|
+
|
|
57
|
+
from boneio.relay import GpioRelay, MCPRelay
|
|
58
|
+
from boneio.sensor import GpioInputSensor
|
|
59
|
+
|
|
60
|
+
_LOGGER = logging.getLogger(__name__)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Manager:
|
|
64
|
+
"""Manager to communicate MQTT with GPIO inputs and outputs."""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
send_message: Callable[[str, Union[str, dict]], None],
|
|
69
|
+
topic_prefix: str,
|
|
70
|
+
relay_pins: List,
|
|
71
|
+
input_pins: List,
|
|
72
|
+
sensors: dict = {},
|
|
73
|
+
modbus: dict = None,
|
|
74
|
+
ha_discovery: bool = True,
|
|
75
|
+
ha_discovery_prefix: str = HOMEASSISTANT,
|
|
76
|
+
mcp23017: Optional[List] = None,
|
|
77
|
+
oled: bool = False,
|
|
78
|
+
adc_list: Optional[List] = None,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Initialize the manager."""
|
|
81
|
+
_LOGGER.info("Initializing manager module.")
|
|
82
|
+
self._loop = asyncio.get_event_loop()
|
|
83
|
+
self._host_data = None
|
|
84
|
+
self._ha_discovery = ha_discovery
|
|
85
|
+
|
|
86
|
+
self.send_message = send_message
|
|
87
|
+
self._topic_prefix = topic_prefix
|
|
88
|
+
self.relay_topic = f"{topic_prefix}/{RELAY}/+/set"
|
|
89
|
+
self._input_pins = input_pins
|
|
90
|
+
self._i2cbusio = I2C(SCL, SDA)
|
|
91
|
+
self._mcp = {}
|
|
92
|
+
self._grouped_outputs = {}
|
|
93
|
+
self._oled = None
|
|
94
|
+
self._tasks: List[asyncio.Task] = []
|
|
95
|
+
self._temp_sensors = []
|
|
96
|
+
self._modbus = None
|
|
97
|
+
if modbus and modbus.get(UART) in UARTS:
|
|
98
|
+
self._modbus = Modbus(UARTS[modbus.get(UART)])
|
|
99
|
+
|
|
100
|
+
def create_temp_sensor(sensor_type: str, temp_def: dict = {}):
|
|
101
|
+
"""Create LM sensor in manager."""
|
|
102
|
+
if sensor_type == LM75:
|
|
103
|
+
from .sensor import LM75Sensor as TempSensor
|
|
104
|
+
elif sensor_type == MCP_TEMP_9808:
|
|
105
|
+
from .sensor import MCP9808Sensor as TempSensor
|
|
106
|
+
else:
|
|
107
|
+
return
|
|
108
|
+
name = temp_def.get(ID)
|
|
109
|
+
id = name.replace(" ", "")
|
|
110
|
+
try:
|
|
111
|
+
temp_sensor = TempSensor(
|
|
112
|
+
id=id,
|
|
113
|
+
name=name,
|
|
114
|
+
i2c=self._i2cbusio,
|
|
115
|
+
address=temp_def[ADDRESS],
|
|
116
|
+
send_message=self.send_message,
|
|
117
|
+
topic_prefix=topic_prefix,
|
|
118
|
+
)
|
|
119
|
+
self.send_ha_autodiscovery(
|
|
120
|
+
id=id,
|
|
121
|
+
name=name,
|
|
122
|
+
ha_type=SENSOR,
|
|
123
|
+
ha_discovery_prefix=ha_discovery_prefix,
|
|
124
|
+
availabilty_msg_func=ha_sensor_temp_availabilty_message,
|
|
125
|
+
)
|
|
126
|
+
self._tasks.append(asyncio.create_task(temp_sensor.send_state()))
|
|
127
|
+
self._temp_sensors.append(temp_sensor)
|
|
128
|
+
except I2CError as err:
|
|
129
|
+
_LOGGER.error("Can't configure Temp sensor. %s", err)
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
def create_mcp23017():
|
|
133
|
+
"""Create MCP23017."""
|
|
134
|
+
for mcp in mcp23017:
|
|
135
|
+
id = mcp[ID] or mcp[ADDRESS]
|
|
136
|
+
try:
|
|
137
|
+
self._mcp[id] = MCP23017(i2c=self._i2cbusio, address=mcp[ADDRESS])
|
|
138
|
+
sleep_time = mcp.get(INIT_SLEEP, 0)
|
|
139
|
+
_LOGGER.debug(
|
|
140
|
+
f"Sleeping for {sleep_time}s while MCP {id} is initializing."
|
|
141
|
+
)
|
|
142
|
+
time.sleep(sleep_time)
|
|
143
|
+
self._grouped_outputs[id] = {}
|
|
144
|
+
except TimeoutError as err:
|
|
145
|
+
_LOGGER.error("Can't connect to MCP %s. %s", id, err)
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
def create_adc():
|
|
149
|
+
"""Create ADC sensor."""
|
|
150
|
+
from boneio.sensor import initialize_adc, GpioADCSensor
|
|
151
|
+
|
|
152
|
+
initialize_adc()
|
|
153
|
+
|
|
154
|
+
# TODO: find what exception can ADC gpio throw.
|
|
155
|
+
for gpio in adc_list:
|
|
156
|
+
name = gpio.get(ID)
|
|
157
|
+
id = name.replace(" ", "")
|
|
158
|
+
pin = gpio[PIN]
|
|
159
|
+
try:
|
|
160
|
+
adc = GpioADCSensor(
|
|
161
|
+
id=id,
|
|
162
|
+
pin=pin,
|
|
163
|
+
name=name,
|
|
164
|
+
send_message=self.send_message,
|
|
165
|
+
topic_prefix=topic_prefix,
|
|
166
|
+
update_interval=gpio.get(UPDATE_INTERVAL, 60),
|
|
167
|
+
)
|
|
168
|
+
self.send_ha_autodiscovery(
|
|
169
|
+
id=id,
|
|
170
|
+
name=name,
|
|
171
|
+
ha_type=SENSOR,
|
|
172
|
+
ha_discovery_prefix=ha_discovery_prefix,
|
|
173
|
+
availabilty_msg_func=ha_adc_sensor_availabilty_message,
|
|
174
|
+
)
|
|
175
|
+
self._tasks.append(asyncio.create_task(adc.send_state()))
|
|
176
|
+
except I2CError as err:
|
|
177
|
+
_LOGGER.error("Can't configure ADC sensor %s. %s", id, err)
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
def create_sdm_630(sensors):
|
|
183
|
+
"""Create SDM sensor for each device."""
|
|
184
|
+
from boneio.sensor.sdm630 import Sdm630
|
|
185
|
+
|
|
186
|
+
for sensor in sensors:
|
|
187
|
+
name = sensor.get(ID)
|
|
188
|
+
id = name.replace(" ", "")
|
|
189
|
+
try:
|
|
190
|
+
sdm = Sdm630(
|
|
191
|
+
modbus=self._modbus,
|
|
192
|
+
address=sensor[ADDRESS],
|
|
193
|
+
id=id,
|
|
194
|
+
name=name,
|
|
195
|
+
send_message=self.send_message,
|
|
196
|
+
topic_prefix=topic_prefix,
|
|
197
|
+
ha_discovery=ha_discovery,
|
|
198
|
+
ha_discovery_prefix=ha_discovery_prefix,
|
|
199
|
+
update_interval=sensor.get(UPDATE_INTERVAL, 30),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
self._tasks.append(asyncio.create_task(sdm.send_state()))
|
|
203
|
+
except I2CError as err:
|
|
204
|
+
_LOGGER.error("Can't configure SDM630 sensor %s. %s", id, err)
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
return True
|
|
208
|
+
|
|
209
|
+
if sensors.get(LM75):
|
|
210
|
+
create_temp_sensor(sensor_type=LM75, temp_def=sensors.get(LM75))
|
|
211
|
+
|
|
212
|
+
if sensors.get(MCP_TEMP_9808):
|
|
213
|
+
create_temp_sensor(
|
|
214
|
+
sensor_type=MCP_TEMP_9808, temp_def=sensors.get(MCP_TEMP_9808)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if sensors.get(SDM630) and self._modbus:
|
|
218
|
+
create_sdm_630(sensors=sensors.get(SDM630))
|
|
219
|
+
|
|
220
|
+
if mcp23017:
|
|
221
|
+
create_mcp23017()
|
|
222
|
+
|
|
223
|
+
if adc_list:
|
|
224
|
+
create_adc()
|
|
225
|
+
|
|
226
|
+
def configure_relay(gpio: dict) -> Any:
|
|
227
|
+
"""Configure kind of relay. Most common MCP."""
|
|
228
|
+
relay_id = gpio[ID].replace(" ", "")
|
|
229
|
+
if gpio[KIND] == MCP:
|
|
230
|
+
mcp_id = gpio.get(MCP_ID, "")
|
|
231
|
+
mcp = self._mcp.get(mcp_id)
|
|
232
|
+
if not mcp:
|
|
233
|
+
_LOGGER.error("No such MCP configured!")
|
|
234
|
+
return
|
|
235
|
+
mcp_relay = MCPRelay(
|
|
236
|
+
pin=int(gpio[PIN]),
|
|
237
|
+
id=gpio[ID],
|
|
238
|
+
send_message=self.send_message,
|
|
239
|
+
topic_prefix=topic_prefix,
|
|
240
|
+
mcp=mcp,
|
|
241
|
+
mcp_id=mcp_id,
|
|
242
|
+
ha_type=gpio[HA_TYPE],
|
|
243
|
+
callback=lambda: self._host_data_callback(mcp_id),
|
|
244
|
+
)
|
|
245
|
+
self._grouped_outputs[mcp_id][relay_id] = mcp_relay
|
|
246
|
+
return mcp_relay
|
|
247
|
+
elif gpio[KIND] == GPIO:
|
|
248
|
+
if GPIO not in self._grouped_outputs:
|
|
249
|
+
self._grouped_outputs[GPIO] = {}
|
|
250
|
+
gpio_relay = GpioRelay(
|
|
251
|
+
pin=gpio[PIN],
|
|
252
|
+
id=gpio[ID],
|
|
253
|
+
send_message=self.send_message,
|
|
254
|
+
topic_prefix=topic_prefix,
|
|
255
|
+
callback=lambda: self._host_data_callback(GPIO),
|
|
256
|
+
)
|
|
257
|
+
self._grouped_outputs[GPIO][relay_id] = gpio_relay
|
|
258
|
+
return gpio_relay
|
|
259
|
+
|
|
260
|
+
self.output = {
|
|
261
|
+
gpio[ID].replace(" ", ""): configure_relay(gpio) for gpio in relay_pins
|
|
262
|
+
}
|
|
263
|
+
for out in self.output.values():
|
|
264
|
+
self.send_ha_autodiscovery(
|
|
265
|
+
id=out.id,
|
|
266
|
+
name=out.name,
|
|
267
|
+
ha_type=out.ha_type,
|
|
268
|
+
ha_discovery_prefix=ha_discovery_prefix,
|
|
269
|
+
availabilty_msg_func=ha_relay_availabilty_message,
|
|
270
|
+
)
|
|
271
|
+
self._loop.call_soon_threadsafe(
|
|
272
|
+
self._loop.call_later,
|
|
273
|
+
0.5,
|
|
274
|
+
out.send_state,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def configure_input(gpio):
|
|
278
|
+
try:
|
|
279
|
+
pin = gpio[PIN]
|
|
280
|
+
input_type = gpio.get(KIND)
|
|
281
|
+
if input_type == SENSOR:
|
|
282
|
+
GpioInputSensor(
|
|
283
|
+
pin=pin,
|
|
284
|
+
press_callback=lambda x, i: self.press_callback(
|
|
285
|
+
x=x,
|
|
286
|
+
inpin=i,
|
|
287
|
+
actions=gpio.get(ACTIONS, {}),
|
|
288
|
+
input_type=INPUT_SENSOR,
|
|
289
|
+
),
|
|
290
|
+
rest_pin=gpio,
|
|
291
|
+
)
|
|
292
|
+
availabilty_msg_func = ha_binary_sensor_availabilty_message
|
|
293
|
+
ha_type = BINARY_SENSOR
|
|
294
|
+
else:
|
|
295
|
+
GpioInputButton(
|
|
296
|
+
pin=pin,
|
|
297
|
+
press_callback=lambda x, i: self.press_callback(
|
|
298
|
+
x=x,
|
|
299
|
+
inpin=i,
|
|
300
|
+
actions=gpio.get(ACTIONS, {}),
|
|
301
|
+
input_type=INPUT,
|
|
302
|
+
),
|
|
303
|
+
rest_pin=gpio,
|
|
304
|
+
)
|
|
305
|
+
availabilty_msg_func = ha_input_availabilty_message
|
|
306
|
+
ha_type = SENSOR
|
|
307
|
+
if gpio.get(SHOW_HA, True):
|
|
308
|
+
self.send_ha_autodiscovery(
|
|
309
|
+
id=pin,
|
|
310
|
+
name=gpio.get(ID, pin),
|
|
311
|
+
ha_type=ha_type,
|
|
312
|
+
ha_discovery_prefix=ha_discovery_prefix,
|
|
313
|
+
availabilty_msg_func=availabilty_msg_func,
|
|
314
|
+
)
|
|
315
|
+
except GPIOInputException as err:
|
|
316
|
+
_LOGGER.error("This PIN %s can't be configured. %s", pin, err)
|
|
317
|
+
pass
|
|
318
|
+
|
|
319
|
+
_LOGGER.info("Initializing inputs. This will take a while.")
|
|
320
|
+
for gpio in self._input_pins:
|
|
321
|
+
configure_input(gpio=gpio)
|
|
322
|
+
|
|
323
|
+
if oled:
|
|
324
|
+
from boneio.oled import Oled
|
|
325
|
+
|
|
326
|
+
self._host_data = HostData(
|
|
327
|
+
output=self._grouped_outputs,
|
|
328
|
+
temp_sensor=self._temp_sensors[0] if self._temp_sensors else None,
|
|
329
|
+
callback=self._host_data_callback,
|
|
330
|
+
)
|
|
331
|
+
for f in host_stats.values():
|
|
332
|
+
self._tasks.append(asyncio.create_task(f(self._host_data)))
|
|
333
|
+
_LOGGER.debug("Gathering host data enabled.")
|
|
334
|
+
try:
|
|
335
|
+
self._oled = Oled(
|
|
336
|
+
host_data=self._host_data, output_groups=list(self._grouped_outputs)
|
|
337
|
+
)
|
|
338
|
+
except (GPIOInputException, I2CError) as err:
|
|
339
|
+
_LOGGER.error("Can't configure OLED display. %s", err)
|
|
340
|
+
|
|
341
|
+
self.send_message(topic=f"{topic_prefix}/{STATE}", payload=ONLINE)
|
|
342
|
+
_LOGGER.info("BoneIO manager is ready.")
|
|
343
|
+
|
|
344
|
+
def _host_data_callback(self, type: str):
|
|
345
|
+
if self._oled:
|
|
346
|
+
self._oled.handle_data_update(type)
|
|
347
|
+
|
|
348
|
+
def get_tasks(self) -> Set[asyncio.Task]:
|
|
349
|
+
return self._tasks
|
|
350
|
+
|
|
351
|
+
def press_callback(
|
|
352
|
+
self, x: ClickTypes, inpin: str, actions: dict, input_type: InputTypes = INPUT
|
|
353
|
+
) -> None:
|
|
354
|
+
"""Press callback to use in input gpio.
|
|
355
|
+
If relay input map is provided also toggle action on relay."""
|
|
356
|
+
topic = f"{self._topic_prefix}/{input_type}/{inpin}"
|
|
357
|
+
self.send_message(topic=topic, payload=x)
|
|
358
|
+
action = actions.get(x)
|
|
359
|
+
if action:
|
|
360
|
+
if action[ACTION] == OUTPUT:
|
|
361
|
+
"""For now only output type is supported"""
|
|
362
|
+
relay = self.output.get(action[PIN].replace(" ", ""))
|
|
363
|
+
if relay:
|
|
364
|
+
relay.toggle()
|
|
365
|
+
# This is similar how Z2M is clearing click sensor.
|
|
366
|
+
self._loop.call_soon_threadsafe(self.send_message, topic, "")
|
|
367
|
+
|
|
368
|
+
def send_ha_autodiscovery(
|
|
369
|
+
self,
|
|
370
|
+
id: str,
|
|
371
|
+
name: str,
|
|
372
|
+
ha_discovery_prefix: str,
|
|
373
|
+
availabilty_msg_func: Callable,
|
|
374
|
+
ha_type: str = "switch",
|
|
375
|
+
topic_prefix: str = None,
|
|
376
|
+
**kwargs,
|
|
377
|
+
) -> None:
|
|
378
|
+
"""Send HA autodiscovery information for each relay."""
|
|
379
|
+
if not self._ha_discovery:
|
|
380
|
+
return
|
|
381
|
+
if not topic_prefix:
|
|
382
|
+
topic_prefix = self._topic_prefix
|
|
383
|
+
msg = availabilty_msg_func(topic=topic_prefix, id=id, name=name, **kwargs)
|
|
384
|
+
topic = f"{ha_discovery_prefix}/{ha_type}/{topic_prefix}/{id}/config"
|
|
385
|
+
_LOGGER.debug("Sending HA discovery for %s, %s.", ha_type, name)
|
|
386
|
+
self.send_message(topic=topic, payload=msg)
|
|
387
|
+
|
|
388
|
+
def receive_message(self, topic: str, message: str) -> None:
|
|
389
|
+
"""Callback for receiving action from Mqtt."""
|
|
390
|
+
extracted_relay = topic.replace(f"{self._topic_prefix}/{RELAY}/", "").replace(
|
|
391
|
+
"/set", ""
|
|
392
|
+
)
|
|
393
|
+
target_device = self.output.get(extracted_relay)
|
|
394
|
+
if target_device:
|
|
395
|
+
if message == ON:
|
|
396
|
+
target_device.turn_on()
|
|
397
|
+
elif message == OFF:
|
|
398
|
+
target_device.turn_off()
|
boneio/modbus.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from boneio.helper import configure_pin
|
|
6
|
+
from boneio.const import ID, RX, TX, UART, REGISTERS
|
|
7
|
+
from pymodbus.client.sync import (
|
|
8
|
+
BaseModbusClient,
|
|
9
|
+
ModbusSerialClient,
|
|
10
|
+
)
|
|
11
|
+
from pymodbus.register_read_message import ReadInputRegistersResponse
|
|
12
|
+
from pymodbus.payload import BinaryPayloadDecoder
|
|
13
|
+
from pymodbus.constants import Endian
|
|
14
|
+
from typing import Any
|
|
15
|
+
from pymodbus.exceptions import ModbusException
|
|
16
|
+
from pymodbus.pdu import ModbusResponse
|
|
17
|
+
|
|
18
|
+
_LOGGER = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Modbus:
|
|
22
|
+
"""Represent modbus connection over chosen UART."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, uart: dict[str, Any]) -> None:
|
|
25
|
+
"""Initialize the Modbus hub."""
|
|
26
|
+
|
|
27
|
+
configure_pin(pin=uart[RX], mode=UART)
|
|
28
|
+
configure_pin(pin=uart[TX], mode=UART)
|
|
29
|
+
self._uart = uart
|
|
30
|
+
|
|
31
|
+
# generic configuration
|
|
32
|
+
self._client: BaseModbusClient | None = None
|
|
33
|
+
self._lock = asyncio.Lock()
|
|
34
|
+
try:
|
|
35
|
+
self._client = ModbusSerialClient(
|
|
36
|
+
port=self._uart[ID],
|
|
37
|
+
method="rtu",
|
|
38
|
+
baudrate=9600,
|
|
39
|
+
stopbits=1,
|
|
40
|
+
bytesize=8,
|
|
41
|
+
parity="N",
|
|
42
|
+
)
|
|
43
|
+
except ModbusException as exception_error:
|
|
44
|
+
_LOGGER.error(exception_error)
|
|
45
|
+
|
|
46
|
+
async def async_close(self) -> None:
|
|
47
|
+
"""Disconnect client."""
|
|
48
|
+
async with self._lock:
|
|
49
|
+
if self._client:
|
|
50
|
+
try:
|
|
51
|
+
self._client.close()
|
|
52
|
+
except ModbusException as exception_error:
|
|
53
|
+
_LOGGER.error(exception_error)
|
|
54
|
+
del self._client
|
|
55
|
+
self._client = None
|
|
56
|
+
_LOGGER.warning("modbus communication closed")
|
|
57
|
+
|
|
58
|
+
def _pymodbus_connect(self) -> bool:
|
|
59
|
+
"""Connect client."""
|
|
60
|
+
try:
|
|
61
|
+
self._client.connect() # type: ignore[union-attr]
|
|
62
|
+
except ModbusException as exception_error:
|
|
63
|
+
_LOGGER.error(exception_error)
|
|
64
|
+
return False
|
|
65
|
+
else:
|
|
66
|
+
_LOGGER.info("modbus communication open")
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
def read_single_register(self, unit: int, address: int, count: int = 2) -> float:
|
|
70
|
+
"""Call sync. pymodbus."""
|
|
71
|
+
if not self._pymodbus_connect:
|
|
72
|
+
_LOGGER.error("Can't connect to Modbus.")
|
|
73
|
+
return None
|
|
74
|
+
kwargs = {"unit": unit, "count": count} if unit else {}
|
|
75
|
+
try:
|
|
76
|
+
result: ReadInputRegistersResponse = self._client.read_input_registers(
|
|
77
|
+
address, **kwargs
|
|
78
|
+
)
|
|
79
|
+
except ModbusException as exception_error:
|
|
80
|
+
_LOGGER.error(exception_error)
|
|
81
|
+
return None
|
|
82
|
+
if not hasattr(result, REGISTERS):
|
|
83
|
+
_LOGGER.error(str(result))
|
|
84
|
+
return None
|
|
85
|
+
return BinaryPayloadDecoder.fromRegisters(
|
|
86
|
+
result.registers, byteorder=Endian.Big, wordorder=Endian.Big
|
|
87
|
+
).decode_32bit_float()
|
|
88
|
+
|
|
89
|
+
def read_multiple_registers(
|
|
90
|
+
self, unit: int, address: int, count: int = 2
|
|
91
|
+
) -> ModbusResponse:
|
|
92
|
+
"""Call sync. pymodbus."""
|
|
93
|
+
if not self._pymodbus_connect:
|
|
94
|
+
_LOGGER.error("Can't connect to Modbus.")
|
|
95
|
+
return None
|
|
96
|
+
kwargs = {"unit": unit, "count": count} if unit else {}
|
|
97
|
+
try:
|
|
98
|
+
result: ReadInputRegistersResponse = self._client.read_input_registers(
|
|
99
|
+
address, **kwargs
|
|
100
|
+
)
|
|
101
|
+
except ModbusException as exception_error:
|
|
102
|
+
_LOGGER.error(exception_error)
|
|
103
|
+
return None
|
|
104
|
+
if not hasattr(result, REGISTERS):
|
|
105
|
+
_LOGGER.error(str(result))
|
|
106
|
+
return None
|
|
107
|
+
return result
|