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/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
@@ -0,0 +1,5 @@
1
+ """Input classes."""
2
+
3
+ from boneio.input.gpio import GpioInputButton
4
+
5
+ __all__ = ["GpioInputButton"]
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