boneIO 0.1.0.dev3__tar.gz → 0.1.0.dev4__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.1.0.dev3 → boneio-0.1.0.dev4}/PKG-INFO +2 -2
- boneio-0.1.0.dev4/boneio/__init__.py +11 -0
- boneio-0.1.0.dev4/boneio/bonecli.py +143 -0
- boneio-0.1.0.dev4/boneio/const.py +55 -0
- boneio-0.1.0.dev4/boneio/helper/__init__.py +4 -0
- boneio-0.1.0.dev4/boneio/helper/gpio.py +30 -0
- boneio-0.1.0.dev4/boneio/helper/stats.py +115 -0
- boneio-0.1.0.dev4/boneio/helper/yaml.py +46 -0
- boneio-0.1.0.dev4/boneio/input/__init__.py +5 -0
- boneio-0.1.0.dev4/boneio/input/gpio.py +147 -0
- boneio-0.1.0.dev4/boneio/manager.py +170 -0
- boneio-0.1.0.dev4/boneio/mqtt_client.py +183 -0
- boneio-0.1.0.dev4/boneio/oled.py +120 -0
- boneio-0.1.0.dev4/boneio/relay/__init__.py +5 -0
- boneio-0.1.0.dev4/boneio/relay/basic.py +68 -0
- boneio-0.1.0.dev4/boneio/relay/gpio.py +42 -0
- boneio-0.1.0.dev4/boneio/relay/mcp.py +45 -0
- boneio-0.1.0.dev4/boneio/version.py +2 -0
- {boneio-0.1.0.dev3 → boneio-0.1.0.dev4}/boneio.egg-info/PKG-INFO +2 -2
- boneio-0.1.0.dev4/boneio.egg-info/SOURCES.txt +25 -0
- boneio-0.1.0.dev4/boneio.egg-info/top_level.txt +1 -0
- boneio-0.1.0.dev3/boneio.egg-info/SOURCES.txt +0 -8
- boneio-0.1.0.dev3/boneio.egg-info/top_level.txt +0 -1
- {boneio-0.1.0.dev3 → boneio-0.1.0.dev4}/README.md +0 -0
- {boneio-0.1.0.dev3 → boneio-0.1.0.dev4}/boneio.egg-info/dependency_links.txt +0 -0
- {boneio-0.1.0.dev3 → boneio-0.1.0.dev4}/boneio.egg-info/entry_points.txt +0 -0
- {boneio-0.1.0.dev3 → boneio-0.1.0.dev4}/boneio.egg-info/requires.txt +0 -0
- {boneio-0.1.0.dev3 → boneio-0.1.0.dev4}/setup.cfg +0 -0
- {boneio-0.1.0.dev3 → boneio-0.1.0.dev4}/setup.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: boneio
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev4
|
|
4
4
|
Summary: Python Helper for BoneIO
|
|
5
5
|
Home-page: https://github.com/maciejk1984/boneIO
|
|
6
6
|
License: GNU General Public License v3.0
|
|
7
|
-
Download-URL: https://github.com/maciejk1984/boneIO/archive/0.1.
|
|
7
|
+
Download-URL: https://github.com/maciejk1984/boneIO/archive/0.1.0dev4.zip
|
|
8
8
|
Platform: UNKNOWN
|
|
9
9
|
Classifier: Programming Language :: Python
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.7
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from .helper import CustomValidator, load_yaml_file
|
|
2
|
+
import click
|
|
3
|
+
import logging
|
|
4
|
+
from colorlog import ColoredFormatter
|
|
5
|
+
from .mqtt_client import MQTTClient
|
|
6
|
+
from .manager import Manager
|
|
7
|
+
from .const import (
|
|
8
|
+
ENABLED,
|
|
9
|
+
GPIO_INPUT,
|
|
10
|
+
HA_DISCOVERY,
|
|
11
|
+
MCP23017,
|
|
12
|
+
OLED,
|
|
13
|
+
OUTPUT,
|
|
14
|
+
PAHO,
|
|
15
|
+
MQTT,
|
|
16
|
+
HOST,
|
|
17
|
+
TOPIC_PREFIX,
|
|
18
|
+
USERNAME,
|
|
19
|
+
PASSWORD,
|
|
20
|
+
)
|
|
21
|
+
from .version import __version__
|
|
22
|
+
import asyncio
|
|
23
|
+
import os
|
|
24
|
+
from functools import wraps
|
|
25
|
+
|
|
26
|
+
_LOGGER = logging.getLogger(__name__)
|
|
27
|
+
logging.basicConfig(level=logging.INFO)
|
|
28
|
+
fmt = "%(asctime)s %(levelname)s (%(threadName)s) [%(name)s] %(message)s"
|
|
29
|
+
datefmt = "%Y-%m-%d %H:%M:%S"
|
|
30
|
+
colorfmt = f"%(log_color)s{fmt}%(reset)s"
|
|
31
|
+
logging.getLogger().handlers[0].setFormatter(
|
|
32
|
+
ColoredFormatter(
|
|
33
|
+
colorfmt,
|
|
34
|
+
datefmt=datefmt,
|
|
35
|
+
reset=True,
|
|
36
|
+
log_colors={
|
|
37
|
+
"DEBUG": "cyan",
|
|
38
|
+
"INFO": "green",
|
|
39
|
+
"WARNING": "yellow",
|
|
40
|
+
"ERROR": "red",
|
|
41
|
+
"CRITICAL": "red",
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
MAINPATH = os.path.join(os.path.dirname(__file__))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def add_options(options):
|
|
50
|
+
def _add_options(func):
|
|
51
|
+
for option in reversed(options):
|
|
52
|
+
func = option(func)
|
|
53
|
+
return func
|
|
54
|
+
|
|
55
|
+
return _add_options
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def coro(f):
|
|
59
|
+
@wraps(f)
|
|
60
|
+
def wrapper(*args, **kwargs):
|
|
61
|
+
return asyncio.run(f(*args, **kwargs))
|
|
62
|
+
|
|
63
|
+
return wrapper
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@click.group(no_args_is_help=True)
|
|
67
|
+
@click.pass_context
|
|
68
|
+
@click.version_option(__version__)
|
|
69
|
+
@coro
|
|
70
|
+
async def cli(ctx):
|
|
71
|
+
"""A tool to run commands."""
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
_options = [
|
|
76
|
+
click.option(
|
|
77
|
+
"-d",
|
|
78
|
+
"--debug",
|
|
79
|
+
default=False,
|
|
80
|
+
count=True,
|
|
81
|
+
help="Set Debug mode. Single debug is debug of this lib. Second d is debug of aioxmpp as well.",
|
|
82
|
+
),
|
|
83
|
+
click.option(
|
|
84
|
+
"--config",
|
|
85
|
+
"-c",
|
|
86
|
+
type=str,
|
|
87
|
+
default="./config.yaml",
|
|
88
|
+
help="Config yaml file. Default to ./config.yaml",
|
|
89
|
+
),
|
|
90
|
+
click.option(
|
|
91
|
+
"--mqttpassword",
|
|
92
|
+
envvar="MQTTPASS",
|
|
93
|
+
type=str,
|
|
94
|
+
help="Mqtt password. To use as ENV named MQTTPASS",
|
|
95
|
+
),
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@cli.command()
|
|
100
|
+
@add_options(_options)
|
|
101
|
+
@click.pass_context
|
|
102
|
+
@coro
|
|
103
|
+
async def run(ctx, debug: int, config: str, mqttpassword: str = ""):
|
|
104
|
+
"""Run BoneIO."""
|
|
105
|
+
if debug == 0:
|
|
106
|
+
logging.getLogger().setLevel(logging.INFO)
|
|
107
|
+
if debug > 0:
|
|
108
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
|
109
|
+
|
|
110
|
+
_LOGGER.info("Debug mode active")
|
|
111
|
+
_LOGGER.debug(f"Lib version is {__version__}")
|
|
112
|
+
if debug > 1:
|
|
113
|
+
logging.getLogger(PAHO).setLevel(logging.DEBUG)
|
|
114
|
+
else:
|
|
115
|
+
logging.getLogger(PAHO).setLevel(logging.WARN)
|
|
116
|
+
schema = load_yaml_file(os.path.join(MAINPATH, "schema.yaml"))
|
|
117
|
+
v = CustomValidator(schema, purge_unknown=True)
|
|
118
|
+
config_yaml = load_yaml_file(config)
|
|
119
|
+
if not config_yaml:
|
|
120
|
+
_LOGGER.info("Missing file.")
|
|
121
|
+
return
|
|
122
|
+
_config = v.normalized(config_yaml)
|
|
123
|
+
client = MQTTClient(
|
|
124
|
+
host=_config[MQTT][HOST],
|
|
125
|
+
username=_config[MQTT].get(USERNAME),
|
|
126
|
+
password=_config[MQTT].get(PASSWORD, mqttpassword),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
manager = Manager(
|
|
130
|
+
send_message=client.send_message,
|
|
131
|
+
topic_prefix=_config[MQTT][TOPIC_PREFIX],
|
|
132
|
+
relay_pins=_config[OUTPUT],
|
|
133
|
+
input_pins=_config[GPIO_INPUT],
|
|
134
|
+
ha_discovery=_config[MQTT][HA_DISCOVERY][ENABLED],
|
|
135
|
+
ha_discovery_prefix=_config[MQTT][HA_DISCOVERY][TOPIC_PREFIX],
|
|
136
|
+
mcp23017=_config[MCP23017],
|
|
137
|
+
oled=_config.get(OLED),
|
|
138
|
+
)
|
|
139
|
+
await client.start_client(manager)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
asyncio.get_event_loop().run_until_complete(cli())
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# from typing import Literal
|
|
2
|
+
from typing_extensions import Literal
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
|
|
5
|
+
DEBOUNCE_DURATION = timedelta(seconds=0.2)
|
|
6
|
+
LONG_PRESS_DURATION = timedelta(seconds=0.7)
|
|
7
|
+
DELAY_DURATION = 0.1
|
|
8
|
+
SECOND_DELAY_DURATION = 0.3
|
|
9
|
+
RELAY = "relay"
|
|
10
|
+
SINGLE = "single"
|
|
11
|
+
DOUBLE = "double"
|
|
12
|
+
LONG = "long"
|
|
13
|
+
ONLINE = "online"
|
|
14
|
+
ON = "ON"
|
|
15
|
+
OFF = "OFF"
|
|
16
|
+
STATE = "state"
|
|
17
|
+
PAHO = "paho.mqtt.client"
|
|
18
|
+
ClickTypes = Literal[SINGLE, DOUBLE, LONG]
|
|
19
|
+
MQTT = "mqtt"
|
|
20
|
+
HOST = "host"
|
|
21
|
+
USERNAME = "username"
|
|
22
|
+
PASSWORD = "password"
|
|
23
|
+
TOPIC_PREFIX = "topic_prefix"
|
|
24
|
+
HA_DISCOVERY = "ha_discovery"
|
|
25
|
+
ENABLED = "enabled"
|
|
26
|
+
GPIO_INPUT = "gpio_input"
|
|
27
|
+
OUTPUT = "output"
|
|
28
|
+
PIN = "pin"
|
|
29
|
+
ID = "id"
|
|
30
|
+
KIND = "kind"
|
|
31
|
+
GPIO = "gpio"
|
|
32
|
+
ACTIONS = "actions"
|
|
33
|
+
ACTION = "action"
|
|
34
|
+
OUTPUT = "output"
|
|
35
|
+
ADDRESS = "address"
|
|
36
|
+
MCP23017 = "mcp23017"
|
|
37
|
+
SWITCH = "switch"
|
|
38
|
+
CONFIG_PIN = "/usr/bin/config-pin"
|
|
39
|
+
MCP = "mcp"
|
|
40
|
+
MCP_ID = "mcp_id"
|
|
41
|
+
HA_TYPE = "ha_type"
|
|
42
|
+
GIGABYTE = 1073741824
|
|
43
|
+
MEGABYTE = 1048576
|
|
44
|
+
OLED_PIN = "P9_41"
|
|
45
|
+
NUM_OF_SCREENS = 6
|
|
46
|
+
WIDTH = 128
|
|
47
|
+
UPTIME = "uptime"
|
|
48
|
+
NETWORK = "network"
|
|
49
|
+
CPU = "cpu"
|
|
50
|
+
DISK = "disk"
|
|
51
|
+
MEMORY = "memory"
|
|
52
|
+
SWAP = "swap"
|
|
53
|
+
WHITE = "white"
|
|
54
|
+
OLED = "oled"
|
|
55
|
+
OledDataTypes = Literal[UPTIME, NETWORK, CPU, DISK, MEMORY, SWAP, OUTPUT]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from Adafruit_BBIO import GPIO
|
|
2
|
+
from Adafruit_BBIO.GPIO import HIGH, LOW
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def setup_output(pin: str):
|
|
6
|
+
"""Set up a GPIO as output."""
|
|
7
|
+
|
|
8
|
+
GPIO.setup(pin, GPIO.OUT, pull_up_down=GPIO.PUD_DOWN)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def setup_input(pin: str, pull_mode: str = "UP"):
|
|
12
|
+
"""Set up a GPIO as input."""
|
|
13
|
+
GPIO.setup(pin, GPIO.IN, GPIO.PUD_DOWN if pull_mode == "DOWN" else GPIO.PUD_UP)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def write_output(pin: str, value: str):
|
|
17
|
+
"""Write a value to a GPIO."""
|
|
18
|
+
|
|
19
|
+
GPIO.output(pin, value)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def read_input(pin: str, on_state=LOW):
|
|
23
|
+
"""Read a value from a GPIO."""
|
|
24
|
+
return GPIO.input(pin) is on_state
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def edge_detect(pin, callback, bounce):
|
|
28
|
+
"""Add detection for RISING and FALLING events."""
|
|
29
|
+
|
|
30
|
+
GPIO.add_event_detect(pin, GPIO.FALLING, callback=callback, bouncetime=bounce)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import psutil
|
|
3
|
+
from math import floor
|
|
4
|
+
from ..const import GIGABYTE, MEGABYTE, NETWORK, DISK, MEMORY, CPU, SWAP, UPTIME
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
intervals = (
|
|
8
|
+
("d", 86400), # 60 * 60 * 24
|
|
9
|
+
("h", 3600), # 60 * 60
|
|
10
|
+
("m", 60),
|
|
11
|
+
("s", 1),
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def display_time(seconds):
|
|
16
|
+
"""Strf time."""
|
|
17
|
+
result = []
|
|
18
|
+
|
|
19
|
+
for name, count in intervals:
|
|
20
|
+
value = seconds // count
|
|
21
|
+
if value:
|
|
22
|
+
seconds -= value * count
|
|
23
|
+
if value == 1:
|
|
24
|
+
name = name.rstrip("s")
|
|
25
|
+
result.append(f"{int(value)}{name}")
|
|
26
|
+
return "".join(result)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def get_network_info(host_data):
|
|
30
|
+
"""Fetch network info."""
|
|
31
|
+
while True:
|
|
32
|
+
net = psutil.net_if_addrs()["eth0"]
|
|
33
|
+
host_data.write(
|
|
34
|
+
NETWORK,
|
|
35
|
+
{"ip": net[0].address, "mask": net[0].netmask, "mac": net[2].address},
|
|
36
|
+
)
|
|
37
|
+
await asyncio.sleep(30)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def get_cpu_info(host_data):
|
|
41
|
+
"""Fetch CPU info."""
|
|
42
|
+
while True:
|
|
43
|
+
cpu = psutil.cpu_times_percent()
|
|
44
|
+
host_data.write(
|
|
45
|
+
CPU,
|
|
46
|
+
{
|
|
47
|
+
"total": f"{int(100 - cpu.idle)}%",
|
|
48
|
+
"user": f"{cpu.user}%",
|
|
49
|
+
"system": f"{cpu.system}%",
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
await asyncio.sleep(5)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def get_disk_info(host_data):
|
|
56
|
+
"""Fetch disk info."""
|
|
57
|
+
while True:
|
|
58
|
+
disk = psutil.disk_usage("/")
|
|
59
|
+
host_data.write(
|
|
60
|
+
DISK,
|
|
61
|
+
{
|
|
62
|
+
"total": f"{floor(disk.total / GIGABYTE)}GB",
|
|
63
|
+
"used": f"{floor(disk.used / GIGABYTE)}GB",
|
|
64
|
+
"free": f"{floor(disk.free / GIGABYTE)}GB",
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
await asyncio.sleep(60)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def get_memory_info(host_data):
|
|
71
|
+
"""Fetch memory info."""
|
|
72
|
+
while True:
|
|
73
|
+
vm = psutil.virtual_memory()
|
|
74
|
+
host_data.write(
|
|
75
|
+
MEMORY,
|
|
76
|
+
{
|
|
77
|
+
"total": f"{floor(vm.total / MEGABYTE)}%",
|
|
78
|
+
"used": f"{floor(vm.used / MEGABYTE)}%",
|
|
79
|
+
"free": f"{floor(vm.available / MEGABYTE)}%",
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
await asyncio.sleep(10)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def get_swap_info(host_data):
|
|
86
|
+
"""Fetch swap info."""
|
|
87
|
+
while True:
|
|
88
|
+
swap = psutil.swap_memory()
|
|
89
|
+
host_data.write(
|
|
90
|
+
SWAP,
|
|
91
|
+
{
|
|
92
|
+
"total": f"{floor(swap.total / MEGABYTE)}%",
|
|
93
|
+
"used": f"{floor(swap.used / MEGABYTE)}%",
|
|
94
|
+
"free": f"{floor(swap.free / MEGABYTE)}%",
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
await asyncio.sleep(10)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def get_uptime(host_data):
|
|
101
|
+
"""Fetch uptime info."""
|
|
102
|
+
while True:
|
|
103
|
+
uptime = display_time(time.clock_gettime(time.CLOCK_BOOTTIME))
|
|
104
|
+
host_data.write_uptime(uptime)
|
|
105
|
+
await asyncio.sleep(10)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
host_stats = {
|
|
109
|
+
NETWORK: get_network_info,
|
|
110
|
+
CPU: get_cpu_info,
|
|
111
|
+
DISK: get_disk_info,
|
|
112
|
+
MEMORY: get_memory_info,
|
|
113
|
+
SWAP: get_swap_info,
|
|
114
|
+
UPTIME: get_uptime,
|
|
115
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
import os
|
|
3
|
+
from cerberus import Validator
|
|
4
|
+
from ..const import ID, OLED, 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,147 @@
|
|
|
1
|
+
"""GPIOInputButton to receive signals."""
|
|
2
|
+
from ..helper.gpio import setup_input, read_input, edge_detect
|
|
3
|
+
import subprocess
|
|
4
|
+
from functools import partial
|
|
5
|
+
from typing import Callable, Any
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
import logging
|
|
8
|
+
import asyncio
|
|
9
|
+
from ..const import (
|
|
10
|
+
CONFIG_PIN,
|
|
11
|
+
GPIO,
|
|
12
|
+
SINGLE,
|
|
13
|
+
DOUBLE,
|
|
14
|
+
LONG,
|
|
15
|
+
ClickTypes,
|
|
16
|
+
DEBOUNCE_DURATION,
|
|
17
|
+
LONG_PRESS_DURATION,
|
|
18
|
+
DELAY_DURATION,
|
|
19
|
+
SECOND_DELAY_DURATION,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_LOGGER = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def configure_pin(pin: str) -> None:
|
|
26
|
+
pin = f"{pin[0:3]}0{pin[3]}" if len(pin) == 4 else pin
|
|
27
|
+
_LOGGER.debug(f"Configuring pin for GPIO {pin}")
|
|
28
|
+
subprocess.run(
|
|
29
|
+
[CONFIG_PIN, pin, GPIO],
|
|
30
|
+
stdout=subprocess.DEVNULL,
|
|
31
|
+
stderr=subprocess.STDOUT,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class GpioInputButton:
|
|
36
|
+
"""Represent Gpio input switch."""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self, pin: str, press_callback: Callable[[ClickTypes, str], None], rest_pin: Any
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Setup GPIO Input Button"""
|
|
42
|
+
self._pin = pin
|
|
43
|
+
configure_pin(pin)
|
|
44
|
+
self._loop = asyncio.get_running_loop()
|
|
45
|
+
self._press_callback = press_callback
|
|
46
|
+
setup_input(self._pin)
|
|
47
|
+
edge_detect(self._pin, callback=self._handle_press, bounce=25)
|
|
48
|
+
self._first_press_timestamp = None
|
|
49
|
+
self._is_long_press = False
|
|
50
|
+
self._second_press_timestamp = None
|
|
51
|
+
self._second_check = False
|
|
52
|
+
_LOGGER.debug("Configured listening for input pin %s", self._pin)
|
|
53
|
+
|
|
54
|
+
def _handle_press(self, pin) -> None:
|
|
55
|
+
"""Handle the button press callback"""
|
|
56
|
+
# Ignore if we are in a long press
|
|
57
|
+
if self._is_long_press:
|
|
58
|
+
return
|
|
59
|
+
now = datetime.now()
|
|
60
|
+
|
|
61
|
+
# Debounce button
|
|
62
|
+
if (
|
|
63
|
+
self._first_press_timestamp is not None
|
|
64
|
+
and now - self._first_press_timestamp < DEBOUNCE_DURATION
|
|
65
|
+
):
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
# Second click debounce. Just in case.
|
|
69
|
+
if (
|
|
70
|
+
self._second_press_timestamp is not None
|
|
71
|
+
and now - self._second_press_timestamp < DEBOUNCE_DURATION
|
|
72
|
+
):
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
if not self._first_press_timestamp:
|
|
76
|
+
self._first_press_timestamp = now
|
|
77
|
+
elif not self._second_press_timestamp:
|
|
78
|
+
self._second_press_timestamp = now
|
|
79
|
+
|
|
80
|
+
self._loop.call_soon_threadsafe(
|
|
81
|
+
self._loop.call_later,
|
|
82
|
+
DELAY_DURATION,
|
|
83
|
+
self.check_press_length,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def is_pressed(self):
|
|
88
|
+
"""Is button pressed."""
|
|
89
|
+
return read_input(self._pin)
|
|
90
|
+
|
|
91
|
+
def check_press_length(self) -> None:
|
|
92
|
+
"""Check if it's a single, double or long press"""
|
|
93
|
+
# Check if button is still pressed
|
|
94
|
+
if self.is_pressed:
|
|
95
|
+
# Schedule a new check
|
|
96
|
+
self._loop.call_soon_threadsafe(
|
|
97
|
+
self._loop.call_later,
|
|
98
|
+
DELAY_DURATION,
|
|
99
|
+
self.check_press_length,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Handle edge case due to multiple clicks
|
|
103
|
+
if self._first_press_timestamp is None:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
# Check if we reached a long press
|
|
107
|
+
diff = datetime.now() - self._first_press_timestamp
|
|
108
|
+
if not self._is_long_press and diff > LONG_PRESS_DURATION:
|
|
109
|
+
self._is_long_press = True
|
|
110
|
+
_LOGGER.debug("Long button press, call callback")
|
|
111
|
+
self._loop.call_soon_threadsafe(
|
|
112
|
+
partial(self._press_callback, LONG, self._pin)
|
|
113
|
+
)
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
# Handle short press
|
|
117
|
+
if not self._is_long_press:
|
|
118
|
+
if not self._second_press_timestamp and not self._second_check:
|
|
119
|
+
# let's try to check if second click will atempt
|
|
120
|
+
self._second_check = True
|
|
121
|
+
self._loop.call_soon_threadsafe(
|
|
122
|
+
self._loop.call_later,
|
|
123
|
+
SECOND_DELAY_DURATION,
|
|
124
|
+
self.check_press_length,
|
|
125
|
+
)
|
|
126
|
+
return
|
|
127
|
+
if self._second_check:
|
|
128
|
+
if self._second_press_timestamp:
|
|
129
|
+
_LOGGER.debug(
|
|
130
|
+
"Double click event, roznica %s",
|
|
131
|
+
self._second_press_timestamp - self._first_press_timestamp,
|
|
132
|
+
)
|
|
133
|
+
self._loop.call_soon_threadsafe(
|
|
134
|
+
partial(self._press_callback, DOUBLE, self._pin)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
elif self._first_press_timestamp:
|
|
138
|
+
_LOGGER.debug("One click event, call callback")
|
|
139
|
+
self._loop.call_soon_threadsafe(
|
|
140
|
+
partial(self._press_callback, SINGLE, self._pin)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Clean state on button released
|
|
144
|
+
self._first_press_timestamp = None
|
|
145
|
+
self._second_press_timestamp = None
|
|
146
|
+
self._second_check = False
|
|
147
|
+
self._is_long_press = False
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
from .input import GpioInputButton
|
|
2
|
+
from .relay import GpioRelay, MCPRelay
|
|
3
|
+
from .oled import Oled
|
|
4
|
+
from .const import (
|
|
5
|
+
ACTION,
|
|
6
|
+
ACTIONS,
|
|
7
|
+
ADDRESS,
|
|
8
|
+
GPIO,
|
|
9
|
+
HA_TYPE,
|
|
10
|
+
ID,
|
|
11
|
+
KIND,
|
|
12
|
+
MCP,
|
|
13
|
+
MCP_ID,
|
|
14
|
+
OUTPUT,
|
|
15
|
+
PIN,
|
|
16
|
+
RELAY,
|
|
17
|
+
ON,
|
|
18
|
+
OFF,
|
|
19
|
+
ONLINE,
|
|
20
|
+
STATE,
|
|
21
|
+
ClickTypes,
|
|
22
|
+
)
|
|
23
|
+
from typing import Callable, Optional, Union, List, Any
|
|
24
|
+
import logging
|
|
25
|
+
import asyncio
|
|
26
|
+
from busio import I2C
|
|
27
|
+
from board import SCL, SDA
|
|
28
|
+
from adafruit_mcp230xx.mcp23017 import MCP23017
|
|
29
|
+
|
|
30
|
+
_LOGGER = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def ha_availibilty_message(topic, relay_id):
|
|
34
|
+
"""Create availability topic for HA."""
|
|
35
|
+
return {
|
|
36
|
+
"availability": [{"topic": f"{topic}/{STATE}"}],
|
|
37
|
+
"command_topic": f"{topic}/relay/{relay_id}/set",
|
|
38
|
+
"device": {
|
|
39
|
+
"identifiers": [topic],
|
|
40
|
+
"manufacturer": "BoneIO",
|
|
41
|
+
"model": "BoneIO Relay Board",
|
|
42
|
+
"name": f"BoneIO {topic}",
|
|
43
|
+
"sw_version": "0.0.1",
|
|
44
|
+
},
|
|
45
|
+
"name": f"Relay {relay_id}",
|
|
46
|
+
"payload_off": OFF,
|
|
47
|
+
"payload_on": ON,
|
|
48
|
+
"state_topic": f"{topic}/{RELAY}/{relay_id}",
|
|
49
|
+
"unique_id": f"{topic}{RELAY}{relay_id}",
|
|
50
|
+
"value_template": "{{ value_json.state }}",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Manager:
|
|
55
|
+
"""Manager to communicate MQTT with GPIO inputs and outputs."""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
send_message: Callable[[str, Union[str, dict]], None],
|
|
60
|
+
topic_prefix: str,
|
|
61
|
+
relay_pins: List,
|
|
62
|
+
input_pins: List,
|
|
63
|
+
ha_discovery: bool = True,
|
|
64
|
+
ha_discovery_prefix: str = "homeassistant",
|
|
65
|
+
mcp23017: Optional[List] = None,
|
|
66
|
+
oled: bool = False,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Initialize the manager."""
|
|
69
|
+
loop = asyncio.get_event_loop()
|
|
70
|
+
|
|
71
|
+
self.send_message = send_message
|
|
72
|
+
self._topic_prefix = topic_prefix
|
|
73
|
+
self.relay_topic = f"{topic_prefix}/{RELAY}/+/set"
|
|
74
|
+
self._input_pins = input_pins
|
|
75
|
+
self._i2cbusio = I2C(SCL, SDA)
|
|
76
|
+
self._mcp = {}
|
|
77
|
+
self._oled = None
|
|
78
|
+
|
|
79
|
+
if mcp23017:
|
|
80
|
+
for mcp in mcp23017:
|
|
81
|
+
self._mcp[mcp[ID] or mcp[ADDRESS]] = MCP23017(
|
|
82
|
+
i2c=self._i2cbusio, address=mcp[ADDRESS]
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def configure_relay(gpio: dict) -> Any:
|
|
86
|
+
"""Configure kind of relay. Most common MCP."""
|
|
87
|
+
if gpio[KIND] == MCP:
|
|
88
|
+
mcp = self._mcp.get(gpio.get(MCP_ID, ""))
|
|
89
|
+
if not mcp:
|
|
90
|
+
_LOGGER.error("No such MCP configured!")
|
|
91
|
+
return
|
|
92
|
+
return MCPRelay(
|
|
93
|
+
pin=int(gpio[PIN]),
|
|
94
|
+
id=gpio[ID],
|
|
95
|
+
send_message=self.send_message,
|
|
96
|
+
topic_prefix=topic_prefix,
|
|
97
|
+
mcp=mcp,
|
|
98
|
+
ha_type=gpio[HA_TYPE],
|
|
99
|
+
)
|
|
100
|
+
elif gpio[KIND] == GPIO:
|
|
101
|
+
return GpioRelay(
|
|
102
|
+
pin=gpio[PIN],
|
|
103
|
+
id=gpio[ID],
|
|
104
|
+
send_message=self.send_message,
|
|
105
|
+
topic_prefix=topic_prefix,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
self.output = {gpio[ID]: configure_relay(gpio) for gpio in relay_pins}
|
|
109
|
+
for out in self.output.values():
|
|
110
|
+
if ha_discovery:
|
|
111
|
+
_LOGGER.debug("Sending HA discovery.")
|
|
112
|
+
self.send_ha_autodiscovery(
|
|
113
|
+
relay=out.id, ha_type=out.ha_type, prefix=ha_discovery_prefix
|
|
114
|
+
)
|
|
115
|
+
loop.call_soon_threadsafe(
|
|
116
|
+
loop.call_later,
|
|
117
|
+
0.5,
|
|
118
|
+
out.send_state,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
self.buttons = [
|
|
122
|
+
GpioInputButton(
|
|
123
|
+
pin=gpio[PIN],
|
|
124
|
+
press_callback=lambda x, i: self.press_callback(x, i, gpio[ACTIONS]),
|
|
125
|
+
rest_pin=gpio,
|
|
126
|
+
)
|
|
127
|
+
for gpio in self._input_pins
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
if oled:
|
|
131
|
+
self._oled = Oled(self.output)
|
|
132
|
+
|
|
133
|
+
self.send_message(topic=f"{topic_prefix}/{STATE}", payload=ONLINE)
|
|
134
|
+
|
|
135
|
+
def get_oled_tasks(self) -> any:
|
|
136
|
+
if not self._oled:
|
|
137
|
+
return []
|
|
138
|
+
return self._oled.get_tasks()
|
|
139
|
+
|
|
140
|
+
def press_callback(self, x: ClickTypes, inpin: str, actions: dict) -> None:
|
|
141
|
+
"""Press callback to use in input gpio.
|
|
142
|
+
If relay input map is provided also toggle action on relay."""
|
|
143
|
+
self.send_message(topic=f"{self._topic_prefix}/input/{inpin}", payload=x)
|
|
144
|
+
action = actions.get(x)
|
|
145
|
+
if action:
|
|
146
|
+
if action[ACTION] == OUTPUT:
|
|
147
|
+
"""For now only output type is supported"""
|
|
148
|
+
output_gpio = self.output.get(action[PIN])
|
|
149
|
+
if output_gpio:
|
|
150
|
+
output_gpio.toggle()
|
|
151
|
+
|
|
152
|
+
def send_ha_autodiscovery(
|
|
153
|
+
self, relay: str, prefix: str, ha_type: str = "switch"
|
|
154
|
+
) -> None:
|
|
155
|
+
"""Send HA autodiscovery information for each relay."""
|
|
156
|
+
msg = ha_availibilty_message(self._topic_prefix, relay_id=relay)
|
|
157
|
+
topic = f"{prefix}/{ha_type}/{self._topic_prefix}/{ha_type}/config"
|
|
158
|
+
self.send_message(topic=topic, payload=msg)
|
|
159
|
+
|
|
160
|
+
def receive_message(self, topic: str, message: str) -> None:
|
|
161
|
+
"""Callback for receiving action from Mqtt."""
|
|
162
|
+
extracted_relay = topic.replace(f"{self._topic_prefix}/{RELAY}/", "").replace(
|
|
163
|
+
"/set", ""
|
|
164
|
+
)
|
|
165
|
+
target_device = self.output.get(extracted_relay)
|
|
166
|
+
if target_device:
|
|
167
|
+
if message == ON:
|
|
168
|
+
target_device.turn_on()
|
|
169
|
+
elif message == OFF:
|
|
170
|
+
target_device.turn_off()
|
|
@@ -0,0 +1,183 @@
|
|
|
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
|
+
from asyncio_mqtt import Client as AsyncioClient, MqttError
|
|
10
|
+
import paho.mqtt.client as mqtt
|
|
11
|
+
from paho.mqtt.properties import Properties
|
|
12
|
+
from paho.mqtt.subscribeoptions import SubscribeOptions
|
|
13
|
+
from .const import PAHO
|
|
14
|
+
|
|
15
|
+
_LOGGER = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MQTTClient:
|
|
19
|
+
"""Represent an MQTT client."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
host: str,
|
|
24
|
+
port: int = 1883,
|
|
25
|
+
**client_options: Any,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Set up client."""
|
|
28
|
+
self.host = host
|
|
29
|
+
self.port = port
|
|
30
|
+
if "client_id" not in client_options:
|
|
31
|
+
client_options["client_id"] = mqtt.base62(uuid.uuid4().int, padding=22)
|
|
32
|
+
if "logger" not in client_options:
|
|
33
|
+
client_options["logger"] = logging.getLogger(PAHO)
|
|
34
|
+
client_options["clean_session"] = True
|
|
35
|
+
self.client_options = client_options
|
|
36
|
+
self.asyncio_client: AsyncioClient = None
|
|
37
|
+
self.create_client()
|
|
38
|
+
self.reconnect_interval = 1
|
|
39
|
+
self.publish_queue: asyncio.Queue = asyncio.Queue()
|
|
40
|
+
|
|
41
|
+
def create_client(self) -> None:
|
|
42
|
+
"""Create the asyncio client."""
|
|
43
|
+
self.asyncio_client = AsyncioClient(
|
|
44
|
+
self.host,
|
|
45
|
+
self.port,
|
|
46
|
+
**self.client_options,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
async def connect(self, *, timeout: float = 10.0) -> None:
|
|
50
|
+
"""Connect to the broker.
|
|
51
|
+
|
|
52
|
+
Can raise asyncio_mqtt.MqttError.
|
|
53
|
+
"""
|
|
54
|
+
await self.asyncio_client.connect(timeout=timeout)
|
|
55
|
+
|
|
56
|
+
async def disconnect(self, *, timeout: float = 10.0) -> None:
|
|
57
|
+
"""Disconnect from the broker.
|
|
58
|
+
|
|
59
|
+
Can raise asyncio_mqtt.MqttError.
|
|
60
|
+
"""
|
|
61
|
+
await self.asyncio_client.disconnect(timeout=timeout)
|
|
62
|
+
|
|
63
|
+
async def publish( # pylint:disable=too-many-arguments
|
|
64
|
+
self,
|
|
65
|
+
topic: str,
|
|
66
|
+
payload: Optional[str] = None,
|
|
67
|
+
qos: int = 0,
|
|
68
|
+
retain: bool = False,
|
|
69
|
+
properties: Optional[Properties] = None,
|
|
70
|
+
timeout: float = 10,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Publish to topic.
|
|
73
|
+
|
|
74
|
+
Can raise asyncio_mqtt.MqttError.
|
|
75
|
+
"""
|
|
76
|
+
params: dict = {"qos": qos, "retain": retain, "timeout": timeout}
|
|
77
|
+
if payload:
|
|
78
|
+
params["payload"] = payload
|
|
79
|
+
if properties:
|
|
80
|
+
params["properties"] = properties
|
|
81
|
+
|
|
82
|
+
_LOGGER.debug("Sending message topic: %s, payload: %s", topic, payload)
|
|
83
|
+
await self.asyncio_client.publish(topic, **params)
|
|
84
|
+
|
|
85
|
+
async def subscribe( # pylint:disable=too-many-arguments
|
|
86
|
+
self,
|
|
87
|
+
topic: str,
|
|
88
|
+
qos: int = 0,
|
|
89
|
+
options: Optional[SubscribeOptions] = None,
|
|
90
|
+
properties: Optional[Properties] = None,
|
|
91
|
+
timeout: float = 10.0,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Subscribe to topic.
|
|
94
|
+
|
|
95
|
+
Can raise asyncio_mqtt.MqttError.
|
|
96
|
+
"""
|
|
97
|
+
params: dict = {"qos": qos, "timeout": timeout}
|
|
98
|
+
if options:
|
|
99
|
+
params["options"] = options
|
|
100
|
+
if properties:
|
|
101
|
+
params["properties"] = properties
|
|
102
|
+
|
|
103
|
+
await self.asyncio_client.subscribe(topic, **params)
|
|
104
|
+
|
|
105
|
+
async def unsubscribe(
|
|
106
|
+
self, topic: str, properties: Optional[Properties] = None, timeout: float = 10.0
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Unsubscribe from topic.
|
|
109
|
+
|
|
110
|
+
Can raise asyncio_mqtt.MqttError.
|
|
111
|
+
"""
|
|
112
|
+
params: dict = {"timeout": timeout}
|
|
113
|
+
if properties:
|
|
114
|
+
params["properties"] = properties
|
|
115
|
+
|
|
116
|
+
await self.asyncio_client.unsubscribe(topic, **params)
|
|
117
|
+
|
|
118
|
+
def send_message(self, topic: str, payload: Union[str, dict]) -> None:
|
|
119
|
+
"""Send a message from the manager options."""
|
|
120
|
+
to_publish = (topic, json.dumps(payload) if type(payload) == dict else payload)
|
|
121
|
+
self.publish_queue.put_nowait(to_publish)
|
|
122
|
+
|
|
123
|
+
async def _handle_publish(self) -> None:
|
|
124
|
+
"""Publish messages as they are put on the queue."""
|
|
125
|
+
while True:
|
|
126
|
+
to_publish: tuple = await self.publish_queue.get()
|
|
127
|
+
await self.publish(*to_publish)
|
|
128
|
+
self.publish_queue.task_done()
|
|
129
|
+
|
|
130
|
+
async def start_client(self, manager: any) -> None:
|
|
131
|
+
"""Start the client with the manager."""
|
|
132
|
+
# Reconnect automatically until the client is stopped.
|
|
133
|
+
while True:
|
|
134
|
+
try:
|
|
135
|
+
await self._subscribe_manager(manager)
|
|
136
|
+
except MqttError as err:
|
|
137
|
+
self.reconnect_interval = min(self.reconnect_interval * 2, 900)
|
|
138
|
+
_LOGGER.error(
|
|
139
|
+
"MQTT error: %s. Reconnecting in %s seconds",
|
|
140
|
+
err,
|
|
141
|
+
self.reconnect_interval,
|
|
142
|
+
)
|
|
143
|
+
await asyncio.sleep(self.reconnect_interval)
|
|
144
|
+
self.create_client() # reset connect/reconnect futures
|
|
145
|
+
|
|
146
|
+
async def _subscribe_manager(self, manager: any) -> None:
|
|
147
|
+
"""Connect and subscribe to manager topics."""
|
|
148
|
+
async with AsyncExitStack() as stack:
|
|
149
|
+
tasks: Set[asyncio.Task] = set()
|
|
150
|
+
|
|
151
|
+
# Connect to the MQTT broker.
|
|
152
|
+
await stack.enter_async_context(self.asyncio_client)
|
|
153
|
+
# Reset the reconnect interval after successful connection.
|
|
154
|
+
self.reconnect_interval = 1
|
|
155
|
+
|
|
156
|
+
publish_task = asyncio.create_task(self._handle_publish())
|
|
157
|
+
tasks.add(publish_task)
|
|
158
|
+
|
|
159
|
+
# Messages that doesn't match a filter will get logged and handled here.
|
|
160
|
+
messages = await stack.enter_async_context(
|
|
161
|
+
self.asyncio_client.unfiltered_messages()
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
messages_task = asyncio.create_task(
|
|
165
|
+
handle_messages(messages, manager.receive_message)
|
|
166
|
+
)
|
|
167
|
+
tasks.add(messages_task)
|
|
168
|
+
|
|
169
|
+
tasks.update(manager.get_oled_tasks())
|
|
170
|
+
|
|
171
|
+
topic = f"{manager.relay_topic}"
|
|
172
|
+
await self.subscribe(topic)
|
|
173
|
+
|
|
174
|
+
# Wait for everything to complete (or fail due to, e.g., network errors).
|
|
175
|
+
await asyncio.gather(*tasks)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def handle_messages(messages: Any, callback: Callable[[str, str], None]) -> None:
|
|
179
|
+
"""Handle messages with callback."""
|
|
180
|
+
async for message in messages:
|
|
181
|
+
payload = message.payload.decode()
|
|
182
|
+
_LOGGER.debug("Received message topic: %s, payload: %s", message.topic, payload)
|
|
183
|
+
callback(message.topic, payload)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from .helper.gpio import setup_input, edge_detect
|
|
2
|
+
from .helper.stats import host_stats
|
|
3
|
+
from .input.gpio import configure_pin
|
|
4
|
+
from .const import (
|
|
5
|
+
OLED_PIN,
|
|
6
|
+
NETWORK,
|
|
7
|
+
CPU,
|
|
8
|
+
DISK,
|
|
9
|
+
MEMORY,
|
|
10
|
+
SWAP,
|
|
11
|
+
NUM_OF_SCREENS,
|
|
12
|
+
UPTIME,
|
|
13
|
+
HOST,
|
|
14
|
+
OUTPUT,
|
|
15
|
+
WHITE,
|
|
16
|
+
OledDataTypes,
|
|
17
|
+
)
|
|
18
|
+
import asyncio
|
|
19
|
+
import os
|
|
20
|
+
import socket
|
|
21
|
+
import logging
|
|
22
|
+
|
|
23
|
+
from functools import partial
|
|
24
|
+
from luma.core.interface.serial import i2c
|
|
25
|
+
from luma.core.render import canvas
|
|
26
|
+
from luma.oled.device import sh1106
|
|
27
|
+
from PIL import ImageFont
|
|
28
|
+
|
|
29
|
+
_LOGGER = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def make_font(name, size):
|
|
33
|
+
font_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "fonts", name))
|
|
34
|
+
return ImageFont.truetype(font_path, size)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
fontBig = make_font("DejaVuSans.ttf", 12)
|
|
38
|
+
fontSmall = make_font("DejaVuSans.ttf", 10)
|
|
39
|
+
|
|
40
|
+
screen_order = [UPTIME, NETWORK, CPU, DISK, MEMORY, SWAP, OUTPUT]
|
|
41
|
+
lines = [17, 32, 47]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class HostData:
|
|
45
|
+
"""Helper class to store host data."""
|
|
46
|
+
|
|
47
|
+
data = {UPTIME: {}, NETWORK: {}, CPU: {}, DISK: {}, MEMORY: {}, SWAP: {}}
|
|
48
|
+
|
|
49
|
+
def __init__(self, output: dict) -> None:
|
|
50
|
+
"""Initialize HostData."""
|
|
51
|
+
self._hostname = socket.gethostname()
|
|
52
|
+
self.data[UPTIME] = {HOST: self._hostname, UPTIME: 0}
|
|
53
|
+
self._output = output
|
|
54
|
+
|
|
55
|
+
def write(self, type: str, data: dict) -> None:
|
|
56
|
+
"""Write data of chosen type."""
|
|
57
|
+
self.data[type] = data
|
|
58
|
+
|
|
59
|
+
def write_uptime(self, uptime: str) -> None:
|
|
60
|
+
"""Write uptime."""
|
|
61
|
+
self.data[UPTIME][UPTIME] = uptime
|
|
62
|
+
|
|
63
|
+
def get(self, type: OledDataTypes) -> dict:
|
|
64
|
+
"""Get saved stats."""
|
|
65
|
+
if type == OUTPUT:
|
|
66
|
+
return self._get_output()
|
|
67
|
+
return self.data[type]
|
|
68
|
+
|
|
69
|
+
def _get_output(self) -> dict:
|
|
70
|
+
"""Get stats for output."""
|
|
71
|
+
out = {}
|
|
72
|
+
for output in self._output.values():
|
|
73
|
+
out[output.id] = output.state
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Oled:
|
|
78
|
+
"""Oled display class."""
|
|
79
|
+
|
|
80
|
+
def __init__(self, output: dict) -> None:
|
|
81
|
+
"""Initialize OLED screen."""
|
|
82
|
+
self._screen_no = 0
|
|
83
|
+
self._loop = asyncio.get_running_loop()
|
|
84
|
+
self._host_data = HostData(output=output)
|
|
85
|
+
configure_pin(OLED_PIN)
|
|
86
|
+
setup_input(OLED_PIN)
|
|
87
|
+
edge_detect(OLED_PIN, callback=self._handle_press, bounce=50)
|
|
88
|
+
serial = i2c(port=2, address=0x3C)
|
|
89
|
+
self._device = sh1106(serial)
|
|
90
|
+
_LOGGER.debug("Configuring OLED screen.")
|
|
91
|
+
|
|
92
|
+
def get_tasks(self) -> list:
|
|
93
|
+
"""Periodically check for host statistics."""
|
|
94
|
+
return [
|
|
95
|
+
asyncio.create_task(func(self._host_data)) for func in host_stats.values()
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
def _handle_press(self, pin: any) -> None:
|
|
99
|
+
"""Handle press of PIN for OLED display."""
|
|
100
|
+
|
|
101
|
+
def press_callback(screen_no):
|
|
102
|
+
screen = screen_order[screen_no]
|
|
103
|
+
data = self._host_data.get(screen)
|
|
104
|
+
with canvas(self._device) as draw:
|
|
105
|
+
draw.text((1, 1), screen, font=fontBig, fill=WHITE)
|
|
106
|
+
i = 0
|
|
107
|
+
for k in data:
|
|
108
|
+
draw.text(
|
|
109
|
+
(3, lines[i]),
|
|
110
|
+
f"{k} {data[k]}",
|
|
111
|
+
font=fontSmall,
|
|
112
|
+
fill=WHITE,
|
|
113
|
+
)
|
|
114
|
+
i += 1
|
|
115
|
+
|
|
116
|
+
self._loop.call_soon_threadsafe(partial(press_callback, self._screen_no))
|
|
117
|
+
if self._screen_no < NUM_OF_SCREENS:
|
|
118
|
+
self._screen_no += 1
|
|
119
|
+
else:
|
|
120
|
+
self._screen_no = 0
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Basic Relay module."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Callable, Union
|
|
5
|
+
from ..const import STATE, RELAY, ON, OFF, SWITCH
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
_LOGGER = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BasicRelay:
|
|
12
|
+
"""Basic relay class."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
send_message: Callable[[str, Union[str, dict]], None],
|
|
17
|
+
topic_prefix: str,
|
|
18
|
+
id: str = None,
|
|
19
|
+
ha_type=SWITCH,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Initialize Basic relay."""
|
|
22
|
+
self._id = id
|
|
23
|
+
self._ha_type = ha_type
|
|
24
|
+
self._send_message = send_message
|
|
25
|
+
self._relay_topic = f"{topic_prefix}/{RELAY}/"
|
|
26
|
+
self._send_topic = f"{self._relay_topic}{self.id}"
|
|
27
|
+
self._state = False
|
|
28
|
+
self._loop = asyncio.get_running_loop()
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def id(self) -> bool:
|
|
32
|
+
"""Id of the relay."""
|
|
33
|
+
return self._id or self._pin
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def state(self) -> bool:
|
|
37
|
+
"""Is relay active."""
|
|
38
|
+
return self._state
|
|
39
|
+
|
|
40
|
+
def send_state(self) -> None:
|
|
41
|
+
"""Send state to Mqtt on action."""
|
|
42
|
+
state = ON if self.is_active else OFF
|
|
43
|
+
self._state = state
|
|
44
|
+
self._send_message(
|
|
45
|
+
topic=self._send_topic,
|
|
46
|
+
payload={STATE: state},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def toggle(self) -> None:
|
|
50
|
+
"""Toggle relay."""
|
|
51
|
+
_LOGGER.debug("Toggle relay.")
|
|
52
|
+
if self.is_active:
|
|
53
|
+
self.turn_off()
|
|
54
|
+
else:
|
|
55
|
+
self.turn_on()
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def is_active(self) -> bool:
|
|
59
|
+
"""Is active check."""
|
|
60
|
+
raise NotImplementedError
|
|
61
|
+
|
|
62
|
+
def turn_on(self) -> None:
|
|
63
|
+
"""Call turn on action."""
|
|
64
|
+
raise NotImplementedError
|
|
65
|
+
|
|
66
|
+
def turn_off(self) -> None:
|
|
67
|
+
"""Call turn off action."""
|
|
68
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""GPIO Relay module.
|
|
2
|
+
!!Not used in BoneIO.
|
|
3
|
+
Created just in case.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from ..helper.gpio import setup_output, read_input, write_output, HIGH, LOW
|
|
7
|
+
from .basic import BasicRelay
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
_LOGGER = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GpioRelay(BasicRelay):
|
|
14
|
+
"""Represents GPIO Relay output"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, pin: str, **kwargs) -> None:
|
|
17
|
+
"""Initialize Gpio relay."""
|
|
18
|
+
super().__init(**kwargs)
|
|
19
|
+
self._pin = pin
|
|
20
|
+
setup_output(self._pin)
|
|
21
|
+
write_output(self.pin, LOW)
|
|
22
|
+
_LOGGER.debug("Setup relay with pin %s", self._pin)
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def is_active(self) -> bool:
|
|
26
|
+
"""Is relay active."""
|
|
27
|
+
return read_input(self.pin, on_state=HIGH)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def pin(self) -> str:
|
|
31
|
+
"""PIN of the relay"""
|
|
32
|
+
return self._pin
|
|
33
|
+
|
|
34
|
+
def turn_on(self) -> None:
|
|
35
|
+
"""Call turn on action."""
|
|
36
|
+
write_output(self.pin, HIGH)
|
|
37
|
+
self._loop.call_soon_threadsafe(self.send_state)
|
|
38
|
+
|
|
39
|
+
def turn_off(self) -> None:
|
|
40
|
+
"""Call turn off action."""
|
|
41
|
+
write_output(self.pin, LOW)
|
|
42
|
+
self._loop.call_soon_threadsafe(self.send_state)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""MCP23017 Relay module."""
|
|
2
|
+
|
|
3
|
+
from .basic import BasicRelay
|
|
4
|
+
from adafruit_mcp230xx.mcp23017 import MCP23017
|
|
5
|
+
import logging
|
|
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, **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._pin.value = False
|
|
20
|
+
_LOGGER.debug("Setup MCP with pin %s", self._pin_id)
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def is_active(self) -> bool:
|
|
24
|
+
"""Is relay active."""
|
|
25
|
+
return self.pin.value
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def pin(self) -> str:
|
|
29
|
+
"""PIN of the relay"""
|
|
30
|
+
return self._pin
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def ha_type(self) -> str:
|
|
34
|
+
"""HA type."""
|
|
35
|
+
return self._ha_type
|
|
36
|
+
|
|
37
|
+
def turn_on(self) -> None:
|
|
38
|
+
"""Call turn on action."""
|
|
39
|
+
self.pin.value = True
|
|
40
|
+
self._loop.call_soon_threadsafe(self.send_state)
|
|
41
|
+
|
|
42
|
+
def turn_off(self) -> None:
|
|
43
|
+
"""Call turn off action."""
|
|
44
|
+
self.pin.value = False
|
|
45
|
+
self._loop.call_soon_threadsafe(self.send_state)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: boneio
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev4
|
|
4
4
|
Summary: Python Helper for BoneIO
|
|
5
5
|
Home-page: https://github.com/maciejk1984/boneIO
|
|
6
6
|
License: GNU General Public License v3.0
|
|
7
|
-
Download-URL: https://github.com/maciejk1984/boneIO/archive/0.1.
|
|
7
|
+
Download-URL: https://github.com/maciejk1984/boneIO/archive/0.1.0dev4.zip
|
|
8
8
|
Platform: UNKNOWN
|
|
9
9
|
Classifier: Programming Language :: Python
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.7
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
boneio/__init__.py
|
|
4
|
+
boneio/bonecli.py
|
|
5
|
+
boneio/const.py
|
|
6
|
+
boneio/manager.py
|
|
7
|
+
boneio/mqtt_client.py
|
|
8
|
+
boneio/oled.py
|
|
9
|
+
boneio/version.py
|
|
10
|
+
boneio.egg-info/PKG-INFO
|
|
11
|
+
boneio.egg-info/SOURCES.txt
|
|
12
|
+
boneio.egg-info/dependency_links.txt
|
|
13
|
+
boneio.egg-info/entry_points.txt
|
|
14
|
+
boneio.egg-info/requires.txt
|
|
15
|
+
boneio.egg-info/top_level.txt
|
|
16
|
+
boneio/helper/__init__.py
|
|
17
|
+
boneio/helper/gpio.py
|
|
18
|
+
boneio/helper/stats.py
|
|
19
|
+
boneio/helper/yaml.py
|
|
20
|
+
boneio/input/__init__.py
|
|
21
|
+
boneio/input/gpio.py
|
|
22
|
+
boneio/relay/__init__.py
|
|
23
|
+
boneio/relay/basic.py
|
|
24
|
+
boneio/relay/gpio.py
|
|
25
|
+
boneio/relay/mcp.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
boneio
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|