boneIO 0.1.0.dev3__tar.gz → 0.1.0.dev5__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.dev5/MANIFEST.in +4 -0
- {boneio-0.1.0.dev3 → boneio-0.1.0.dev5}/PKG-INFO +11 -3
- boneio-0.1.0.dev5/boneio/__init__.py +11 -0
- boneio-0.1.0.dev5/boneio/bonecli.py +143 -0
- boneio-0.1.0.dev5/boneio/config.example.yaml +38 -0
- boneio-0.1.0.dev5/boneio/config.yaml +20 -0
- boneio-0.1.0.dev5/boneio/const.py +55 -0
- boneio-0.1.0.dev5/boneio/helper/__init__.py +4 -0
- boneio-0.1.0.dev5/boneio/helper/gpio.py +30 -0
- boneio-0.1.0.dev5/boneio/helper/stats.py +115 -0
- boneio-0.1.0.dev5/boneio/helper/yaml.py +46 -0
- boneio-0.1.0.dev5/boneio/input/__init__.py +5 -0
- boneio-0.1.0.dev5/boneio/input/gpio.py +147 -0
- boneio-0.1.0.dev5/boneio/manager.py +170 -0
- boneio-0.1.0.dev5/boneio/mqtt_client.py +183 -0
- boneio-0.1.0.dev5/boneio/oled.py +120 -0
- boneio-0.1.0.dev5/boneio/relay/__init__.py +5 -0
- boneio-0.1.0.dev5/boneio/relay/basic.py +68 -0
- boneio-0.1.0.dev5/boneio/relay/gpio.py +42 -0
- boneio-0.1.0.dev5/boneio/relay/mcp.py +45 -0
- boneio-0.1.0.dev5/boneio/schema.yaml +167 -0
- boneio-0.1.0.dev5/boneio/version.py +2 -0
- {boneio-0.1.0.dev3 → boneio-0.1.0.dev5}/boneio.egg-info/PKG-INFO +11 -3
- boneio-0.1.0.dev5/boneio.egg-info/SOURCES.txt +30 -0
- boneio-0.1.0.dev5/boneio.egg-info/top_level.txt +1 -0
- boneio-0.1.0.dev5/requirements.txt +45 -0
- {boneio-0.1.0.dev3 → boneio-0.1.0.dev5}/setup.py +5 -1
- 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.dev5}/README.md +0 -0
- {boneio-0.1.0.dev3 → boneio-0.1.0.dev5}/boneio.egg-info/dependency_links.txt +0 -0
- {boneio-0.1.0.dev3 → boneio-0.1.0.dev5}/boneio.egg-info/entry_points.txt +0 -0
- {boneio-0.1.0.dev3 → boneio-0.1.0.dev5}/boneio.egg-info/requires.txt +0 -0
- {boneio-0.1.0.dev3 → boneio-0.1.0.dev5}/setup.cfg +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.dev5
|
|
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.0.dev5.zip
|
|
8
8
|
Platform: UNKNOWN
|
|
9
9
|
Classifier: Programming Language :: Python
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.7
|
|
@@ -12,5 +12,13 @@ Classifier: Programming Language :: Python :: 3.8
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.9
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Example usage:
|
|
16
|
+
boneio run -dd -c config.yaml
|
|
17
|
+
|
|
18
|
+
Install needed:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
sudo apt-get install libopenjp2-7-dev libatlas-base-dev
|
|
22
|
+
```
|
|
23
|
+
|
|
16
24
|
|
|
@@ -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,38 @@
|
|
|
1
|
+
mqtt:
|
|
2
|
+
host: mqtt.hass
|
|
3
|
+
username: mqtt
|
|
4
|
+
password: pass
|
|
5
|
+
topic_prefix: boneiohome
|
|
6
|
+
ha_discovery:
|
|
7
|
+
enabled: yes
|
|
8
|
+
|
|
9
|
+
i2c:
|
|
10
|
+
id: myb
|
|
11
|
+
sda: P9_18
|
|
12
|
+
scl: P9_17
|
|
13
|
+
|
|
14
|
+
oled:
|
|
15
|
+
|
|
16
|
+
mcp23017:
|
|
17
|
+
- id: mcp1
|
|
18
|
+
address: 0x20
|
|
19
|
+
|
|
20
|
+
gpio_input:
|
|
21
|
+
- id: "one"
|
|
22
|
+
pin: P8_12
|
|
23
|
+
- id: "two"
|
|
24
|
+
pin: P8_7
|
|
25
|
+
actions:
|
|
26
|
+
single:
|
|
27
|
+
action: output
|
|
28
|
+
pin: myoutput
|
|
29
|
+
|
|
30
|
+
output: !include output.yaml
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
##OUTPUT.YAML
|
|
34
|
+
- id: myoutput
|
|
35
|
+
kind: mcp
|
|
36
|
+
mcp_id: mcp1
|
|
37
|
+
pin: 0
|
|
38
|
+
ha_type: light
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
mqtt:
|
|
2
|
+
host: mqtt.hass
|
|
3
|
+
username: mqtt
|
|
4
|
+
password: pass
|
|
5
|
+
# ha_discovery:
|
|
6
|
+
# enabled: True
|
|
7
|
+
|
|
8
|
+
# i2c:
|
|
9
|
+
# - id: myb
|
|
10
|
+
# sda: "P9_18"
|
|
11
|
+
# scl: "P9_17"
|
|
12
|
+
|
|
13
|
+
gpio_input:
|
|
14
|
+
- id: "one"
|
|
15
|
+
pin: "P8_3"
|
|
16
|
+
|
|
17
|
+
output:
|
|
18
|
+
- id: myinput
|
|
19
|
+
kind: gpio
|
|
20
|
+
pin: P9_17
|
|
@@ -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
|