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/mqtt_client.py ADDED
@@ -0,0 +1,170 @@
1
+ """Provide an MQTT client for providing BoneIO MQTT broker."""
2
+ import asyncio
3
+ import json
4
+ import logging
5
+ import uuid
6
+ from contextlib import AsyncExitStack
7
+ from typing import Any, Callable, Optional, Set, Union
8
+
9
+ import paho.mqtt.client as mqtt
10
+ from asyncio_mqtt import Client as AsyncioClient
11
+ from asyncio_mqtt import MqttError
12
+ from paho.mqtt.properties import Properties
13
+ from paho.mqtt.subscribeoptions import SubscribeOptions
14
+
15
+ from boneio.const import PAHO
16
+ from boneio.helper import UniqueQueue
17
+
18
+ _LOGGER = logging.getLogger(__name__)
19
+
20
+
21
+ class MQTTClient:
22
+ """Represent an MQTT client."""
23
+
24
+ def __init__(
25
+ self,
26
+ host: str,
27
+ port: int = 1883,
28
+ **client_options: Any,
29
+ ) -> None:
30
+ """Set up client."""
31
+ self.host = host
32
+ self.port = port
33
+ if "client_id" not in client_options:
34
+ client_options["client_id"] = mqtt.base62(uuid.uuid4().int, padding=22)
35
+ if "logger" not in client_options:
36
+ client_options["logger"] = logging.getLogger(PAHO)
37
+ client_options["clean_session"] = True
38
+ self.client_options = client_options
39
+ self.asyncio_client: AsyncioClient = None
40
+ self.create_client()
41
+ self.reconnect_interval = 1
42
+ self.publish_queue: UniqueQueue = UniqueQueue()
43
+
44
+ def create_client(self) -> None:
45
+ """Create the asyncio client."""
46
+ self.asyncio_client = AsyncioClient(
47
+ self.host,
48
+ self.port,
49
+ **self.client_options,
50
+ )
51
+
52
+ async def publish( # pylint:disable=too-many-arguments
53
+ self,
54
+ topic: str,
55
+ payload: Optional[str] = None,
56
+ qos: int = 0,
57
+ retain: bool = False,
58
+ properties: Optional[Properties] = None,
59
+ timeout: float = 10,
60
+ ) -> None:
61
+ """Publish to topic.
62
+
63
+ Can raise asyncio_mqtt.MqttError.
64
+ """
65
+ params: dict = {"qos": qos, "retain": retain, "timeout": timeout}
66
+ if payload:
67
+ params["payload"] = payload
68
+ if properties:
69
+ params["properties"] = properties
70
+
71
+ _LOGGER.debug("Sending message topic: %s, payload: %s", topic, payload)
72
+ await self.asyncio_client.publish(topic, **params)
73
+
74
+ async def subscribe( # pylint:disable=too-many-arguments
75
+ self,
76
+ topic: str,
77
+ qos: int = 0,
78
+ options: Optional[SubscribeOptions] = None,
79
+ properties: Optional[Properties] = None,
80
+ timeout: float = 10.0,
81
+ ) -> None:
82
+ """Subscribe to topic.
83
+
84
+ Can raise asyncio_mqtt.MqttError.
85
+ """
86
+ params: dict = {"qos": qos, "timeout": timeout}
87
+ if options:
88
+ params["options"] = options
89
+ if properties:
90
+ params["properties"] = properties
91
+
92
+ await self.asyncio_client.subscribe(topic, **params)
93
+
94
+ async def unsubscribe(
95
+ self, topic: str, properties: Optional[Properties] = None, timeout: float = 10.0
96
+ ) -> None:
97
+ """Unsubscribe from topic.
98
+
99
+ Can raise asyncio_mqtt.MqttError.
100
+ """
101
+ params: dict = {"timeout": timeout}
102
+ if properties:
103
+ params["properties"] = properties
104
+
105
+ await self.asyncio_client.unsubscribe(topic, **params)
106
+
107
+ def send_message(self, topic: str, payload: Union[str, dict]) -> None:
108
+ """Send a message from the manager options."""
109
+ to_publish = (topic, json.dumps(payload) if type(payload) == dict else payload)
110
+ self.publish_queue.put_nowait(to_publish)
111
+
112
+ async def _handle_publish(self) -> None:
113
+ """Publish messages as they are put on the queue."""
114
+ while True:
115
+ to_publish: tuple = await self.publish_queue.get()
116
+ await self.publish(*to_publish)
117
+ self.publish_queue.task_done()
118
+
119
+ async def start_client(self, manager: any) -> None:
120
+ """Start the client with the manager."""
121
+ # Reconnect automatically until the client is stopped.
122
+ while True:
123
+ try:
124
+ await self._subscribe_manager(manager)
125
+ except MqttError as err:
126
+ self.reconnect_interval = min(self.reconnect_interval * 2, 900)
127
+ _LOGGER.error(
128
+ "MQTT error: %s. Reconnecting in %s seconds",
129
+ err,
130
+ self.reconnect_interval,
131
+ )
132
+ await asyncio.sleep(self.reconnect_interval)
133
+ self.create_client() # reset connect/reconnect futures
134
+
135
+ async def _subscribe_manager(self, manager: any) -> None:
136
+ """Connect and subscribe to manager topics + host stats."""
137
+ async with AsyncExitStack() as stack:
138
+ tasks: Set[asyncio.Task] = set()
139
+
140
+ # Connect to the MQTT broker.
141
+ await stack.enter_async_context(self.asyncio_client)
142
+ # Reset the reconnect interval after successful connection.
143
+ self.reconnect_interval = 1
144
+
145
+ publish_task = asyncio.create_task(self._handle_publish())
146
+ tasks.add(publish_task)
147
+
148
+ # Messages that doesn't match a filter will get logged and handled here.
149
+ messages = await stack.enter_async_context(
150
+ self.asyncio_client.unfiltered_messages()
151
+ )
152
+
153
+ messages_task = asyncio.create_task(
154
+ handle_messages(messages, manager.receive_message)
155
+ )
156
+ tasks.add(messages_task)
157
+
158
+ topic = f"{manager.relay_topic}"
159
+ await self.subscribe(topic)
160
+
161
+ # Wait for everything to complete (or fail due to, e.g., network errors).
162
+ await asyncio.gather(*tasks)
163
+
164
+
165
+ async def handle_messages(messages: Any, callback: Callable[[str, str], None]) -> None:
166
+ """Handle messages with callback."""
167
+ async for message in messages:
168
+ payload = message.payload.decode()
169
+ _LOGGER.debug("Received message topic: %s, payload: %s", message.topic, payload)
170
+ callback(message.topic, payload)
boneio/oled.py ADDED
@@ -0,0 +1,118 @@
1
+ import logging
2
+ from itertools import cycle
3
+ from typing import List
4
+
5
+ from luma.core.interface.serial import i2c
6
+ from luma.core.render import canvas
7
+ from luma.oled.device import sh1106
8
+ from luma.core.error import DeviceNotFoundError
9
+ from PIL import ImageDraw
10
+
11
+ from boneio.const import CPU, DISK, MEMORY, NETWORK, OLED_PIN, SWAP, UPTIME, WHITE
12
+ from boneio.helper import (
13
+ HostData,
14
+ configure_pin,
15
+ edge_detect,
16
+ make_font,
17
+ setup_input,
18
+ I2CError,
19
+ )
20
+
21
+ _LOGGER = logging.getLogger(__name__)
22
+
23
+
24
+ fontBig = make_font("DejaVuSans.ttf", 12)
25
+ fontSmall = make_font("DejaVuSans.ttf", 10)
26
+ fontExtraSmall = make_font("DejaVuSans.ttf", 7)
27
+ danube = make_font("danube__.ttf", 15, local=True)
28
+
29
+ screen_order = [UPTIME, NETWORK, CPU, DISK, MEMORY, SWAP]
30
+
31
+ STANDARD_ROWS = [17, 32, 47]
32
+ UPTIME_ROWS = list(range(25, 50, 10))
33
+ OUTPUT_ROWS = list(range(14, 60, 6))
34
+ OUTPUT_COLS = range(0, 101, 50)
35
+
36
+
37
+ class Oled:
38
+ """Oled display class."""
39
+
40
+ def __init__(self, host_data: HostData, output_groups: List[str]) -> None:
41
+ """Initialize OLED screen."""
42
+ self._screen_order = cycle(screen_order + output_groups)
43
+ self._output_groups = output_groups
44
+ self._current_screen = next(self._screen_order)
45
+ self._host_data = host_data
46
+ configure_pin(OLED_PIN)
47
+ setup_input(OLED_PIN)
48
+ edge_detect(pin=OLED_PIN, callback=self._handle_press, bounce=80)
49
+ try:
50
+ serial = i2c(port=2, address=0x3C)
51
+ self._device = sh1106(serial)
52
+ except DeviceNotFoundError as err:
53
+ raise I2CError(err)
54
+ _LOGGER.debug("Configuring OLED screen.")
55
+
56
+ def _draw_standard(self, data: dict, draw: ImageDraw) -> None:
57
+ """Draw standard information about host screen."""
58
+ draw.text((1, 1), self._current_screen, font=fontBig, fill=WHITE)
59
+ i = 0
60
+ for k in data:
61
+ draw.text(
62
+ (3, STANDARD_ROWS[i]),
63
+ f"{k} {data[k]}",
64
+ font=fontSmall,
65
+ fill=WHITE,
66
+ )
67
+ i += 1
68
+
69
+ def _draw_uptime(self, data: dict, draw: ImageDraw) -> None:
70
+ """Draw uptime screen with boneIO logo."""
71
+ draw.text((3, 5), "bone", font=danube, fill=WHITE)
72
+ draw.text((53, 5), "iO", font=danube, fill=WHITE)
73
+ i = 0
74
+ for k in data:
75
+ draw.text(
76
+ (3, UPTIME_ROWS[i]),
77
+ f"{k}: {data[k]}",
78
+ font=fontSmall,
79
+ fill=WHITE,
80
+ )
81
+ i += 1
82
+
83
+ def _draw_output(self, data: dict, draw: ImageDraw) -> None:
84
+ "Draw outputs of GPIO/MCP relays."
85
+ cols = cycle(OUTPUT_COLS)
86
+ draw.text((1, 1), f"Relay {self._current_screen}", font=fontSmall, fill=WHITE)
87
+ i = 0
88
+ j = next(cols)
89
+ for k in data:
90
+ if len(OUTPUT_ROWS) == i:
91
+ j = next(cols)
92
+ i = 0
93
+ draw.text(
94
+ (j, OUTPUT_ROWS[i]), f"{k} {data[k]}", font=fontExtraSmall, fill=WHITE
95
+ )
96
+ i += 1
97
+
98
+ def render_display(self) -> None:
99
+ """Render display."""
100
+ data = self._host_data.get(self._current_screen)
101
+ if data:
102
+ with canvas(self._device) as draw:
103
+ if self._current_screen in self._output_groups:
104
+ self._draw_output(data, draw)
105
+ elif self._current_screen == UPTIME:
106
+ self._draw_uptime(data, draw)
107
+ else:
108
+ self._draw_standard(data, draw)
109
+
110
+ def handle_data_update(self, type: str):
111
+ """Callback to handle new data present into screen."""
112
+ if type == self._current_screen:
113
+ self.render_display()
114
+
115
+ def _handle_press(self, pin: any) -> None:
116
+ """Handle press of PIN for OLED display."""
117
+ self._current_screen = next(self._screen_order)
118
+ self.render_display()
@@ -0,0 +1,5 @@
1
+ """Relay module."""
2
+ from boneio.relay.gpio import GpioRelay
3
+ from boneio.relay.mcp import MCPRelay
4
+
5
+ __all__ = ["MCPRelay", "GpioRelay"]
boneio/relay/basic.py ADDED
@@ -0,0 +1,76 @@
1
+ """Basic Relay module."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import Callable
6
+
7
+ from boneio.const import OFF, ON, RELAY, STATE, SWITCH
8
+ from boneio.helper import BasicMqtt
9
+
10
+ _LOGGER = logging.getLogger(__name__)
11
+
12
+
13
+ class BasicRelay(BasicMqtt):
14
+ """Basic relay class."""
15
+
16
+ def __init__(
17
+ self, callback: Callable, id: str = None, ha_type=SWITCH, **kwargs
18
+ ) -> None:
19
+ """Initialize Basic relay."""
20
+ super().__init__(id=id, name=id, topic_type=RELAY, **kwargs)
21
+ self._ha_type = ha_type
22
+ self._state = False
23
+ self._callback = callback
24
+ self._loop = asyncio.get_running_loop()
25
+
26
+ @property
27
+ def is_mcp_type(self) -> bool:
28
+ """Check if relay is mcp type."""
29
+ return False
30
+
31
+ @property
32
+ def id(self) -> bool:
33
+ """Id of the relay.
34
+ Has to be trimmed out of spaces because of MQTT handling in HA."""
35
+ return self._id or self._pin
36
+
37
+ @property
38
+ def name(self) -> bool:
39
+ """Not trimmed id."""
40
+ return self._name or self._pin
41
+
42
+ @property
43
+ def state(self) -> bool:
44
+ """Is relay active."""
45
+ return self._state
46
+
47
+ def send_state(self) -> None:
48
+ """Send state to Mqtt on action."""
49
+ state = ON if self.is_active else OFF
50
+ self._state = state
51
+ self._send_message(
52
+ topic=self._send_topic,
53
+ payload={STATE: state},
54
+ )
55
+ self._loop.call_soon_threadsafe(self._callback)
56
+
57
+ def toggle(self) -> None:
58
+ """Toggle relay."""
59
+ _LOGGER.debug("Toggle relay.")
60
+ if self.is_active:
61
+ self.turn_off()
62
+ else:
63
+ self.turn_on()
64
+
65
+ @property
66
+ def is_active(self) -> bool:
67
+ """Is active check."""
68
+ raise NotImplementedError
69
+
70
+ def turn_on(self) -> None:
71
+ """Call turn on action."""
72
+ raise NotImplementedError
73
+
74
+ def turn_off(self) -> None:
75
+ """Call turn off action."""
76
+ raise NotImplementedError
boneio/relay/gpio.py ADDED
@@ -0,0 +1,44 @@
1
+ """GPIO Relay module.
2
+ !!Not used in BoneIO.
3
+ Created just in case.
4
+ """
5
+
6
+ import logging
7
+
8
+ from boneio.helper import read_input, setup_output, write_output
9
+ from boneio.relay.basic import BasicRelay
10
+ from boneio.const import HIGH, LOW
11
+
12
+ _LOGGER = logging.getLogger(__name__)
13
+
14
+
15
+ class GpioRelay(BasicRelay):
16
+ """Represents GPIO Relay output"""
17
+
18
+ def __init__(self, pin: str, **kwargs) -> None:
19
+ """Initialize Gpio relay."""
20
+ super().__init(**kwargs)
21
+ self._pin = pin
22
+ setup_output(self._pin)
23
+ write_output(self.pin, LOW)
24
+ _LOGGER.debug("Setup relay with pin %s", self._pin)
25
+
26
+ @property
27
+ def is_active(self) -> bool:
28
+ """Is relay active."""
29
+ return read_input(self.pin, on_state=HIGH)
30
+
31
+ @property
32
+ def pin(self) -> str:
33
+ """PIN of the relay"""
34
+ return self._pin
35
+
36
+ def turn_on(self) -> None:
37
+ """Call turn on action."""
38
+ write_output(self.pin, HIGH)
39
+ self._loop.call_soon_threadsafe(self.send_state)
40
+
41
+ def turn_off(self) -> None:
42
+ """Call turn off action."""
43
+ write_output(self.pin, LOW)
44
+ self._loop.call_soon_threadsafe(self.send_state)
boneio/relay/mcp.py ADDED
@@ -0,0 +1,56 @@
1
+ """MCP23017 Relay module."""
2
+
3
+ import logging
4
+ from adafruit_mcp230xx.mcp23017 import MCP23017
5
+ from boneio.relay.basic import BasicRelay
6
+
7
+ _LOGGER = logging.getLogger(__name__)
8
+
9
+
10
+ class MCPRelay(BasicRelay):
11
+ """Represents MCP Relay output"""
12
+
13
+ def __init__(self, pin: int, mcp: MCP23017, mcp_id: str, **kwargs) -> None:
14
+ """Initialize MCP relay."""
15
+ super().__init__(**kwargs)
16
+ self._pin_id = pin
17
+ self._pin = mcp.get_pin(pin)
18
+ self._pin.switch_to_output(value=True)
19
+ self._mcp_id = mcp_id
20
+ self._pin.value = False
21
+ _LOGGER.debug("Setup MCP with pin %s", self._pin_id)
22
+
23
+ @property
24
+ def is_mcp_type(self) -> bool:
25
+ """Check if relay is mcp type."""
26
+ return True
27
+
28
+ @property
29
+ def mcp_id(self) -> str:
30
+ """Retrieve parent MCP ID."""
31
+ return self._mcp_id
32
+
33
+ @property
34
+ def is_active(self) -> bool:
35
+ """Is relay active."""
36
+ return self.pin.value
37
+
38
+ @property
39
+ def pin(self) -> str:
40
+ """PIN of the relay"""
41
+ return self._pin
42
+
43
+ @property
44
+ def ha_type(self) -> str:
45
+ """HA type."""
46
+ return self._ha_type
47
+
48
+ def turn_on(self) -> None:
49
+ """Call turn on action."""
50
+ self.pin.value = True
51
+ self._loop.call_soon_threadsafe(self.send_state)
52
+
53
+ def turn_off(self) -> None:
54
+ """Call turn off action."""
55
+ self.pin.value = False
56
+ self._loop.call_soon_threadsafe(self.send_state)