second commit
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
'''
|
||||
Devices
|
||||
|
||||
Created on 23 août 2022
|
||||
|
||||
@author: denis@e-educ.fr
|
||||
|
||||
Flow and Frequency output of the YF-S201
|
||||
|
||||
Flow(L/H) Frequency(Hz)
|
||||
120 16
|
||||
240 32.5
|
||||
360 49.3
|
||||
480 65.5
|
||||
600 82
|
||||
720 90.2
|
||||
|
||||
F = 7.5 Q Q: l/mn
|
||||
'''
|
||||
import logging, threading, random
|
||||
from modules import sensors as dev, utils, gpiolib as gpio
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
class YF_S201(dev.InputDevice):
|
||||
waterflow_timeout = 2
|
||||
pulse_counter = 0
|
||||
start_counter = False
|
||||
stop_counter = None
|
||||
|
||||
def __init__(self, pin, register=None, publish=None):
|
||||
super().__init__(pin, register=register, callback=self.on_count_pulse, pull_up_down=gpio.IO_PULL_UP,)
|
||||
self.publish = publish
|
||||
threading.Thread(target=self.waterflow_process, args=()).start()
|
||||
|
||||
def waterflow_publish(self, flow=None):
|
||||
if self.publish is not None:
|
||||
self.publish(dict(
|
||||
uuid=self.key,
|
||||
topic="waterflow",
|
||||
ts=utils.ts_now(),
|
||||
flow=flow,
|
||||
)
|
||||
)
|
||||
|
||||
def on_count_pulse(self, channel):
|
||||
if self.start_counter:
|
||||
self.pulse_counter += 1
|
||||
|
||||
def wait_for_counts(self, timeout):
|
||||
self.pulse_counter = 0
|
||||
self.start_counter = True
|
||||
threading.Event().wait(timeout)
|
||||
self.start_counter = False
|
||||
|
||||
def waterflow_process(self):
|
||||
self.stop_counter = threading.Event()
|
||||
while not self.stop_counter.is_set():
|
||||
self.wait_for_counts(self.waterflow_timeout)
|
||||
flow = self.pulse_counter / 7.5
|
||||
if flow:
|
||||
logger.info(f'\nwaterflow_process flow = {flow}')
|
||||
self.waterflow_publish(flow)
|
||||
|
||||
def stop(self):
|
||||
self.stop_counter.set()
|
||||
|
||||
|
||||
class Relay(dev.RelayFromShiftOut):
|
||||
|
||||
def __init__(self, shift_out_register, register=None, publish=None):
|
||||
super().__init__(shift_out_register, register, relay_timeout=1.0)
|
||||
self.publish = publish
|
||||
self.start_time, self.end_time = 0, 0
|
||||
self.started = False
|
||||
self.duration_timer = threading.Event()
|
||||
self.water_flow_timer = threading.Event()
|
||||
|
||||
self.flow, self.rate = 0.00, 0.00
|
||||
self.flow_meter_start()
|
||||
self.relay_publish('init', index=self.index, label=self.label, dev_type=self.dev_type)
|
||||
|
||||
def now(self):
|
||||
return utils.ts_now_s()
|
||||
|
||||
def relay_publish(self, topic, **kwargs):
|
||||
if self.publish is not None:
|
||||
self.publish(topic, **dict(
|
||||
uuid=self.key,
|
||||
ts=utils.ts_now_s(),
|
||||
relay_state=self.is_on(),
|
||||
elapsed=self.time_elapsed(),
|
||||
remaining=self.time_remaining(),
|
||||
flow=self.flow,
|
||||
rate=self.rate,
|
||||
**kwargs
|
||||
)
|
||||
)
|
||||
|
||||
def time_elapsed(self):
|
||||
elapsed = self.now() - self.start_time
|
||||
if self.start_time <= 0 or elapsed <= 0:
|
||||
elapsed = 0
|
||||
return elapsed
|
||||
|
||||
def time_remaining(self):
|
||||
remaining = self.end_time - self.now()
|
||||
if remaining < 0:
|
||||
remaining = 0
|
||||
return remaining
|
||||
|
||||
def is_it_over(self, timeout):
|
||||
elapsed = self.now() - self.start_time
|
||||
if elapsed > timeout:
|
||||
self.duration_timer.set()
|
||||
|
||||
def _open(self, timeout):
|
||||
self.duration_timer = threading.Event()
|
||||
self.started = True
|
||||
while not self.duration_timer.is_set():
|
||||
try:
|
||||
if self.started:
|
||||
self.is_it_over(timeout)
|
||||
threading.Event().wait(1.0)
|
||||
except Exception as e:
|
||||
logger.error(f'run error {e}')
|
||||
self.start_time = 0
|
||||
self.relay_stop()
|
||||
|
||||
def relay_start(self, duration=0):
|
||||
self.start_time = self.now()
|
||||
self.end_time = self.start_time + duration
|
||||
self.open()
|
||||
if duration > 0:
|
||||
threading.Thread(target=self._open, args=(duration, )).start()
|
||||
self.rate, self.flow = 0.00, 0.00
|
||||
self.begin = self.now()
|
||||
self.water_flow_timer.set()
|
||||
self.relay_publish('start')
|
||||
|
||||
def relay_stop(self):
|
||||
self.duration_timer.set()
|
||||
self.close()
|
||||
self.started = False
|
||||
self.water_flow_timer.clear()
|
||||
self.relay_publish('stop')
|
||||
|
||||
def relay_halt(self):
|
||||
if self.is_on():
|
||||
self.started = False
|
||||
|
||||
def relay_restart(self):
|
||||
if self.is_on():
|
||||
self.started = True
|
||||
|
||||
def relay_status(self):
|
||||
self.relay_publish('status')
|
||||
|
||||
def _flow_meter(self):
|
||||
while True:
|
||||
try:
|
||||
self.water_flow_timer.wait()
|
||||
elapsed = (self.now() - self.begin)/60
|
||||
self.flow = random.randint(1050, 1065)/100 * elapsed
|
||||
self.rate = self.flow / elapsed if elapsed > 0 else 0.00
|
||||
threading.Event().wait(1.0)
|
||||
except Exception as e:
|
||||
logger.error(f'flow_meter_process error {e}')
|
||||
threading.Event().wait(1.0)
|
||||
|
||||
def flow_meter_start(self):
|
||||
self.begin = self.now()
|
||||
threading.Thread(target=self._flow_meter).start()
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
'''
|
||||
Created on 21 août 2022
|
||||
|
||||
@author: denis
|
||||
'''
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
if os.getenv('BOARD_DEVICE') == 'RPI':
|
||||
print("found a RPI board")
|
||||
import RPi.GPIO as GPIO
|
||||
logger.info(GPIO.RPI_INFO) # @UndefinedVariable
|
||||
GPIO.setmode(GPIO.BOARD) # @UndefinedVariable
|
||||
|
||||
elif os.getenv('BOARD_DEVICE') == 'RPI1B':
|
||||
print("found a RPI 1B board")
|
||||
import RPi.GPIO as GPIO # @Reimport
|
||||
logger.info(GPIO.RPI_INFO) # @UndefinedVariable
|
||||
GPIO.setmode(GPIO.BOARD) # @UndefinedVariable
|
||||
|
||||
elif os.getenv('BOARD_DEVICE') == 'OPIWIN+':
|
||||
print("found an OPI WIN+ board")
|
||||
import orangepi.winplus
|
||||
from OPi import GPIO # @Reimport
|
||||
GPIO.setmode(orangepi.winplus.BOARD)
|
||||
|
||||
elif os.getenv('BOARD_DEVICE') == 'OPIZERO':
|
||||
print("found an OPI ZERO board")
|
||||
import orangepi.zero
|
||||
from OPi import GPIO # @Reimport
|
||||
GPIO.setmode(orangepi.zero.BOARD)
|
||||
|
||||
GPIO.setwarnings(False) # @UndefinedVariable
|
||||
|
||||
IO_RISING = GPIO.RISING # @UndefinedVariable
|
||||
IO_FALLING = GPIO.FALLING # @UndefinedVariable
|
||||
IO_BOTH = GPIO.BOTH # @UndefinedVariable
|
||||
IO_LOW = GPIO.LOW # @UndefinedVariable
|
||||
IO_HIGH = GPIO.HIGH # @UndefinedVariable
|
||||
IO_INPUT = GPIO.IN # @UndefinedVariable
|
||||
IO_OUTPUT = GPIO.OUT # @UndefinedVariable
|
||||
|
||||
IO_PULL_UP = GPIO.PUD_UP
|
||||
IO_PULL_DOWN = GPIO.PUD_DOWN
|
||||
IO_PULL_OFF = GPIO.PUD_OFF
|
||||
|
||||
IO_CLEANUP = GPIO.cleanup # @UndefinedVariable
|
||||
|
||||
lock = threading.Lock()
|
||||
|
||||
|
||||
def bitread(b, bitpos):
|
||||
return (b>>bitpos) & 0x1
|
||||
|
||||
class Pin:
|
||||
def __init__(self, channel, sens, initial=None, callback=None, edge=IO_FALLING, bouncetime=500, pull_up_down=GPIO.PUD_OFF):
|
||||
self.channel = channel
|
||||
self.sens = sens
|
||||
|
||||
lock.acquire()
|
||||
if initial in [GPIO.LOW, GPIO.HIGH] and sens==GPIO.OUT:
|
||||
GPIO.setup(channel, sens, initial=initial, pull_up_down=pull_up_down)
|
||||
else:
|
||||
GPIO.setup(channel, sens, pull_up_down=pull_up_down)
|
||||
|
||||
if callback is not None:
|
||||
self.set_interrupt(edge, callback, bouncetime)
|
||||
lock.release()
|
||||
|
||||
logger.info(f'Channel {self.channel} sens: {sens} initial: {initial}')
|
||||
self.value = self.read() # initial value
|
||||
|
||||
|
||||
def cleanup(self):
|
||||
lock.acquire()
|
||||
GPIO.cleanup(self.channel)
|
||||
lock.release()
|
||||
|
||||
|
||||
def set_interrupt(self, edge, callback, bouncetime=None):
|
||||
if callable(callback) and edge in [IO_RISING, IO_FALLING, IO_BOTH]:
|
||||
GPIO.add_event_detect(self.channel, edge, callback=callback, bouncetime=bouncetime)
|
||||
|
||||
|
||||
def remove_interrupt(self):
|
||||
lock.acquire()
|
||||
GPIO.remove_event_detect(self.channel)
|
||||
lock.release()
|
||||
|
||||
|
||||
def state(self):
|
||||
return self.value
|
||||
|
||||
def read(self):
|
||||
lock.acquire()
|
||||
self.value = GPIO.input(self.channel)
|
||||
lock.release()
|
||||
#logger.info(f'Read {self.channel} -> {self.value}')
|
||||
return self.value
|
||||
|
||||
def write(self, value):
|
||||
lock.acquire()
|
||||
GPIO.output(self.channel, value)
|
||||
lock.release()
|
||||
#logger.info(f'Write {self.channel} <- {value}')
|
||||
return self.read()
|
||||
|
||||
|
||||
class ShiftInRegister74HC165():
|
||||
lastState = 0;
|
||||
currentState = 0;
|
||||
|
||||
def __init__(self, latch_pin, clock_pin, data_pin, number=1):
|
||||
self.datalen = number * 8
|
||||
self.latch_pin = Pin(latch_pin, IO_OUTPUT, initial=IO_LOW, pull_up_down=IO_PULL_DOWN)
|
||||
self.clock_pin = Pin(clock_pin, IO_OUTPUT, initial=IO_LOW, pull_up_down=IO_PULL_DOWN)
|
||||
self.data_pin = Pin(data_pin, IO_INPUT, initial=IO_LOW, pull_up_down=IO_PULL_DOWN)
|
||||
threading.Event().wait(0.5)
|
||||
|
||||
def read(self):
|
||||
self.lastState = self.currentState
|
||||
result = 0
|
||||
self.latch_pin.write(IO_HIGH)
|
||||
for i in range(self.datalen):
|
||||
value = self.data_pin.read()
|
||||
result |= (value << ((self.datalen-1) - i))
|
||||
self.clock_pin.write(IO_HIGH)
|
||||
self.clock_pin.write(IO_LOW)
|
||||
self.latch_pin.write(IO_LOW)
|
||||
self.currentState = result
|
||||
return result
|
||||
|
||||
def state(self, idx):
|
||||
return bitread(self.currentState, idx)
|
||||
|
||||
def last(self, idx):
|
||||
return bitread(self.lastState, idx)
|
||||
|
||||
def update(self):
|
||||
return self.read()!=self.lastState
|
||||
|
||||
def hasChanged(self, idx=None):
|
||||
return self.lastState!=self.currentState if idx is None else self.state(idx)!=self.last(idx)
|
||||
|
||||
def pressed(self, idx):
|
||||
return not self.last(idx) and self.state(idx)
|
||||
|
||||
def released(self, idx):
|
||||
return self.last(idx) and not self.state(idx)
|
||||
|
||||
def cleanup(self):
|
||||
self.latch_pin.cleanup()
|
||||
self.clock_pin.cleanup()
|
||||
self.data_pin.cleanup()
|
||||
|
||||
|
||||
class ShiftOutRegister74HC595():
|
||||
|
||||
def __init__(self, latch_pin, clock_pin, data_pin, number=1):
|
||||
self.datalen = number * 8
|
||||
self.latch_pin = Pin(latch_pin, IO_OUTPUT, initial=IO_LOW)
|
||||
self.clock_pin = Pin(clock_pin, IO_OUTPUT, initial=IO_LOW)
|
||||
self.data_pin = Pin(data_pin, IO_OUTPUT, initial=IO_LOW)
|
||||
self.set_low()
|
||||
threading.Event().wait(0.5)
|
||||
|
||||
def set_low(self):
|
||||
self.registers = [0] * self.datalen
|
||||
self.write()
|
||||
|
||||
def write(self):
|
||||
self.clock_pin.write(IO_LOW)
|
||||
self.latch_pin.write(IO_LOW)
|
||||
self.clock_pin.write(IO_HIGH)
|
||||
#
|
||||
#for i in range(self.datalen):
|
||||
for bit in self.registers:
|
||||
self.clock_pin.write(IO_LOW)
|
||||
self.data_pin.write(int(bit))
|
||||
self.clock_pin.write(IO_HIGH)
|
||||
#
|
||||
self.clock_pin.write(IO_LOW)
|
||||
self.latch_pin.write(IO_HIGH)
|
||||
self.clock_pin.write(IO_HIGH)
|
||||
|
||||
def set(self, index, state):
|
||||
self.registers[index] = int(state)
|
||||
self.write()
|
||||
return int(state)
|
||||
|
||||
def get(self, index):
|
||||
return self.registers[index]
|
||||
|
||||
def cleanup(self):
|
||||
self.set_low()
|
||||
self.latch_pin.cleanup()
|
||||
self.clock_pin.cleanup()
|
||||
self.data_pin.cleanup()
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
'''
|
||||
Created on 29 août 2022
|
||||
|
||||
@author: denis
|
||||
'''
|
||||
import logging
|
||||
import threading
|
||||
from RPLCD.i2c import CharLCD
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
#lcd = CharLCD(settings.LCD_EXPANDER, settings.LCD_ADDR, port=settings.LCD_PORT)
|
||||
|
||||
|
||||
class LCD():
|
||||
def __init__(self, lcd, timeout=None):
|
||||
self.lcd = lcd
|
||||
self.waiter = threading.Event()
|
||||
self.cols = 16
|
||||
self.timeout = timeout
|
||||
self.screen_enabled = True
|
||||
|
||||
@property
|
||||
def screen_enabled(self):
|
||||
return self._enabled
|
||||
|
||||
@screen_enabled.setter
|
||||
def screen_enabled(self, v):
|
||||
self.lcd.backlight_enabled = v
|
||||
self._enabled = v
|
||||
|
||||
|
||||
def create_char(self, pos, cc):
|
||||
self.lcd.create_char(pos, cc)
|
||||
|
||||
|
||||
# LCD display
|
||||
def _display(self, event, msg, timeout):
|
||||
self.screen_enabled = True
|
||||
self.lcd.clear()
|
||||
self.lcd.write_string(msg)
|
||||
event.wait(timeout)
|
||||
self.screen_enabled = False
|
||||
|
||||
|
||||
def write_msg(self, msg, timeout=5):
|
||||
if not self.waiter.is_set():
|
||||
self.waiter.set()
|
||||
self.waiter = threading.Event()
|
||||
threading.Thread(target=self._display, args=(self.waiter, msg, timeout, )).start()
|
||||
|
||||
|
||||
def _screen_enable(self, event, timeout):
|
||||
self.screen_enabled = True
|
||||
event.wait(timeout)
|
||||
self.screen_enabled = False
|
||||
|
||||
|
||||
def screen_disable(self):
|
||||
if not self.waiter.is_set():
|
||||
self.waiter.set()
|
||||
self.screen_enabled = False
|
||||
|
||||
|
||||
def screen_enable(self):
|
||||
if self.timeout is not None:
|
||||
if not self.waiter.is_set():
|
||||
self.waiter.set()
|
||||
self.waiter = threading.Event()
|
||||
threading.Thread(target=self._screen_enable, args=(self.waiter, self.timeout, )).start()
|
||||
else:
|
||||
self.screen_enabled = True
|
||||
|
||||
|
||||
def display(self, msg, pos=(0, 0), align=None, clear=True):
|
||||
self.screen_enable()
|
||||
|
||||
if clear:
|
||||
self.lcd.clear()
|
||||
self.lcd.cursor_pos = pos
|
||||
if align=='left':
|
||||
self.lcd.write_string(msg.ljust(self.cols))
|
||||
elif align=='right':
|
||||
self.lcd.write_string(msg.rjust(self.cols))
|
||||
else:
|
||||
self.lcd.write_string(msg)
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
#
|
||||
# mqtt service
|
||||
#
|
||||
import json, time, logging, ssl
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MqttBase(object):
|
||||
def __init__(self, **p):
|
||||
super().__init__()
|
||||
|
||||
self.host = p.get('host')
|
||||
self.port = p.get('port')
|
||||
self.username = p.get('username')
|
||||
self.password = p.get('password')
|
||||
self.keepalive = p.get('keepalive')
|
||||
self.use_ssl = p.get('use_ssl', False)
|
||||
self.ca_cert = p.get('ca_cert')
|
||||
self.tls_version = p.get('tls_version', ssl.PROTOCOL_TLSv1_2)
|
||||
self.subscriptions = [(topic, qos) for topic, qos in p.get('topic_subs', [])]
|
||||
self.unsubs = self.client_get_unsubs()
|
||||
self.topic_base = p.get('topic_base', '')
|
||||
|
||||
self.on_message_callback = p.get('on_messages', self._on_message_callback)
|
||||
self.on_bytes_callback = p.get('on_bytes', self._on_bytes_callback)
|
||||
# mqtt Client
|
||||
self.client = mqtt.Client()
|
||||
if self.username:
|
||||
self.client.username_pw_set(username=self.username, password=self.password)
|
||||
if self.use_ssl and self.ca_cert:
|
||||
self.client.tls_set(ca_certs=self.ca_cert, tls_version=self.tls_version)
|
||||
|
||||
self.client.on_connect = self._on_connect
|
||||
self.client.on_disconnect = self._on_disconnect
|
||||
self.client.on_message = self._on_message
|
||||
self.client.on_log = self._on_log
|
||||
|
||||
|
||||
def client_get_unsubs(self):
|
||||
return [topic for topic, _ in self.subscriptions ]
|
||||
|
||||
|
||||
def client_set_subscriptions(self):
|
||||
self.client.unsubscribe(self.client_get_unsubs())
|
||||
self.client.subscribe(self.subscriptions)
|
||||
logger.info(f"\n client_set_subscription {self.subscriptions}")
|
||||
|
||||
|
||||
def client_add_subscriptions(self, subs=[]):
|
||||
self.subscriptions += subs
|
||||
self.subscriptions = list(set(self.subscriptions))
|
||||
|
||||
|
||||
def _publish_message(self, topic, **payload):
|
||||
try:
|
||||
qos = payload.pop('qos', 0)
|
||||
retain = payload.pop('retain', False)
|
||||
message = json.dumps(payload)
|
||||
self.client.publish(topic, payload=message.encode('utf-8'), qos=qos, retain=retain)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
|
||||
def _publish_bytes(self, topic, payload, **conf):
|
||||
try:
|
||||
qos = conf.pop('qos', 0)
|
||||
retain = conf.pop('retain', False)
|
||||
self.client.publish(topic, payload=payload, qos=qos, retain=retain)
|
||||
except Exception as e:
|
||||
logger.error(f"\n _publish_bytes error: {e}")
|
||||
|
||||
|
||||
def _on_log(self, mqttc, obj, level, string): # @UnusedVariable
|
||||
pass
|
||||
|
||||
|
||||
def _on_connect_info(self, info):
|
||||
logger.info(info)
|
||||
|
||||
|
||||
def _on_connect(self, client, userdata, flags, rc):
|
||||
msg = f"Client mqtt\n {client._client_id} connected\n status {rc}\n host={self.host}:{self.port}\n username={self.username}"
|
||||
try:
|
||||
if rc:
|
||||
raise
|
||||
self.client_set_subscriptions()
|
||||
self._on_connect_info(msg)
|
||||
except Exception as e:
|
||||
logger.error(f"\n _on_connect error {e}")
|
||||
|
||||
|
||||
|
||||
def _on_disconnect(self, client, userdata, rc):
|
||||
logger.info(f"Client mqtt\n Disconnected: {client._client_id} with status {rc}")
|
||||
try:
|
||||
j = 3
|
||||
for i in range(j):
|
||||
#client.unsubscribe(self.unsubs)
|
||||
try:
|
||||
client.reconnect()
|
||||
break
|
||||
except Exception as e:
|
||||
if i < j:
|
||||
logger.warn(e)
|
||||
time.sleep(1)
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Client mqtt\n _on_disconnect error {e}")
|
||||
|
||||
|
||||
def _on_stop_mqtt(self):
|
||||
pass
|
||||
|
||||
|
||||
def _on_message_callback(self, topic, payload):
|
||||
pass
|
||||
|
||||
|
||||
def _on_bytes_callback(self, topic, payload):
|
||||
pass
|
||||
|
||||
|
||||
def _on_message(self, client, userdata, message):
|
||||
try:
|
||||
try:
|
||||
payload = json.loads(message.payload.decode("utf-8"))
|
||||
self.on_message_callback(message.topic, payload)
|
||||
except:
|
||||
self.on_bytes_callback(message.topic, message.payload)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"\n==== _on_message error {e}\n {message.topic} {message.payload[:80]}")
|
||||
|
||||
|
||||
def connectMQTT(self):
|
||||
self.client.connect_async(self.host, self.port, self.keepalive)
|
||||
|
||||
|
||||
def loop_forever(self):
|
||||
self.connectMQTT()
|
||||
self.client.loop_forever()
|
||||
|
||||
|
||||
def startMQTT(self):
|
||||
self.connectMQTT()
|
||||
self.client.loop_start()
|
||||
|
||||
|
||||
def stopMQTT(self):
|
||||
self._on_stop_mqtt()
|
||||
self.client.loop_stop()
|
||||
self.client.disconnect()
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
'''
|
||||
Created on 22 août 2022
|
||||
|
||||
@author: denis
|
||||
'''
|
||||
import logging
|
||||
import threading
|
||||
from . import gpiolib as gpio
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
class DeviceRegister:
|
||||
|
||||
def __init__(self, register:dict):
|
||||
"""
|
||||
register must contain
|
||||
{'dev_type': 'None', 'index': 0, 'key': 'default', 'label': 'Default label', 'status': 0, 'active': 1}
|
||||
"""
|
||||
self.register = register.copy()
|
||||
|
||||
@property
|
||||
def dev_type(self):
|
||||
return self.register.get('dev_type')
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self.register.get('status')
|
||||
|
||||
@status.setter
|
||||
def status(self, v):
|
||||
self.register['status'] = int(v)
|
||||
|
||||
@property
|
||||
def active_pin(self):
|
||||
return self.register.get('active')
|
||||
|
||||
@property
|
||||
def key(self):
|
||||
return self.register.get('key')
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
return self.register.get('label')
|
||||
|
||||
@property
|
||||
def index(self):
|
||||
return self.register.get('index')
|
||||
|
||||
class Device(DeviceRegister):
|
||||
|
||||
def __init__(self, pin, register=None, sens=gpio.IO_OUTPUT, initial=None, callback=None, edge=gpio.IO_FALLING, bouncetime=500, pull_up_down=None):
|
||||
super().__init__(register)
|
||||
|
||||
self.pin = gpio.Pin(pin, sens=sens, initial=initial, callback=callback, edge=edge, bouncetime=bouncetime, pull_up_down=pull_up_down)
|
||||
self.sens = sens
|
||||
self.active = gpio.IO_HIGH if self.active_pin else gpio.IO_LOW
|
||||
self.not_active = gpio.IO_LOW if self.active_pin else gpio.IO_HIGH
|
||||
self.is_opened = self.is_on
|
||||
self.is_closed = self.is_off
|
||||
self.open = self.on
|
||||
self.close = self.off
|
||||
|
||||
if self.sens == gpio.IO_OUTPUT:
|
||||
if initial==gpio.IO_HIGH:
|
||||
self.on()
|
||||
elif initial==gpio.IO_LOW:
|
||||
self.off()
|
||||
|
||||
def state(self):
|
||||
return self.pin.read()
|
||||
|
||||
def on(self):
|
||||
if self.sens == gpio.IO_OUTPUT:
|
||||
return self.pin.write(self.active)
|
||||
|
||||
def off(self):
|
||||
if self.sens == gpio.IO_OUTPUT:
|
||||
return self.pin.write(self.not_active)
|
||||
|
||||
def is_on(self):
|
||||
return self.state() == self.active
|
||||
|
||||
def is_off(self):
|
||||
return self.state() == self.not_active
|
||||
|
||||
def toggle(self):
|
||||
if self.is_on():
|
||||
return self.off()
|
||||
if self.is_off():
|
||||
return self.on()
|
||||
|
||||
class Led(Device):
|
||||
|
||||
def __init__(self, pin, register=None, initial=gpio.IO_LOW):
|
||||
super().__init__(pin, register=register, sens=gpio.IO_OUTPUT, initial=initial)
|
||||
|
||||
class Relay(Device):
|
||||
|
||||
def __init__(self, pin, register=None, initial=gpio.IO_LOW):
|
||||
super().__init__(pin, register=register, sens=gpio.IO_OUTPUT, initial=initial)
|
||||
|
||||
class Switch(Device):
|
||||
|
||||
def __init__(self, pin, register=None, callback=None, edge=gpio.IO_BOTH):
|
||||
super().__init__(pin, register=register, callback=callback, sens=gpio.IO_INPUT, edge=edge)
|
||||
|
||||
def on(self):
|
||||
pass
|
||||
|
||||
def off(self):
|
||||
pass
|
||||
|
||||
class Button(Device):
|
||||
|
||||
def __init__(self, pin, register=None, callback=None, bouncetime=500):
|
||||
super().__init__(pin, register=register, callback=callback, sens=gpio.IO_INPUT, edge=gpio.IO_FALLING, bouncetime=bouncetime)
|
||||
self._state = self.not_active
|
||||
|
||||
def state(self):
|
||||
return self._state
|
||||
|
||||
def on(self):
|
||||
self._state = self.active
|
||||
|
||||
def off(self):
|
||||
self._state = self.not_active
|
||||
|
||||
class InputDevice(Device):
|
||||
|
||||
def __init__(self, pin, register=None, callback=None, pull_up_down=gpio.IO_PULL_UP):
|
||||
super().__init__(pin, register=register, callback=callback, sens=gpio.IO_INPUT, edge=gpio.IO_FALLING, pull_up_down=pull_up_down)
|
||||
|
||||
def on(self):
|
||||
pass
|
||||
|
||||
def off(self):
|
||||
pass
|
||||
|
||||
class RelayFromShiftOut(DeviceRegister):
|
||||
|
||||
def __init__(self, shift_out_register, register=None, relay_timeout=0, closed=True):
|
||||
super().__init__(register)
|
||||
self.shift_out_register = shift_out_register
|
||||
|
||||
self.max_index = shift_out_register.datalen
|
||||
self.active = gpio.IO_HIGH if self.active_pin else gpio.IO_LOW
|
||||
self.not_active = gpio.IO_LOW if self.active_pin else gpio.IO_HIGH
|
||||
|
||||
self.relay_timeout = 0
|
||||
self.is_opened = self.is_on
|
||||
self.is_closed = self.is_off
|
||||
self.open = self.on
|
||||
self.close = self.off
|
||||
|
||||
if closed:
|
||||
self.off()
|
||||
else:
|
||||
self.on()
|
||||
|
||||
self.relay_timeout = relay_timeout
|
||||
|
||||
logger.info(f'RelayShiftOut Init index {self.index}')
|
||||
|
||||
def wait_after_event(self, timeout):
|
||||
if timeout:
|
||||
threading.Event().wait(timeout)
|
||||
|
||||
def on(self):
|
||||
self.status = self.shift_out_register.set(self.index, self.active)
|
||||
self.wait_after_event(self.relay_timeout) # wait after opening
|
||||
logger.info(f'RelayShiftOut {self.index} on')
|
||||
return self.status
|
||||
|
||||
def off(self):
|
||||
self.status = self.shift_out_register.set(self.index, self.not_active)
|
||||
self.wait_after_event(self.relay_timeout) # wait after closing
|
||||
logger.info(f'RelayShiftOut {self.index} off')
|
||||
return self.status
|
||||
|
||||
def is_on(self):
|
||||
self.status = self.shift_out_register.get(self.index) == self.active
|
||||
return self.status
|
||||
|
||||
def is_off(self):
|
||||
self.status = self.shift_out_register.get(self.index) == self.not_active
|
||||
return self.status
|
||||
|
||||
def toggle(self):
|
||||
if self.is_on():
|
||||
return self.off()
|
||||
if self.is_off():
|
||||
return self.on()
|
||||
@@ -0,0 +1,138 @@
|
||||
'''
|
||||
Created on 3 févr. 2026
|
||||
|
||||
@author: denis
|
||||
'''
|
||||
from django.conf import settings
|
||||
|
||||
# myapp/system_stats.py
|
||||
import threading
|
||||
import time
|
||||
import os
|
||||
import psutil
|
||||
|
||||
# intervale de mise à jour (secondes)
|
||||
REFRESH_INTERVAL = 5
|
||||
RAMDISK = settings.RAMDISK_DEVICE
|
||||
|
||||
_cache = {
|
||||
"shm": [],
|
||||
"cpu_info": {},
|
||||
"memory_info": {},
|
||||
"disk_info": {},
|
||||
"ramdisk_info": {},
|
||||
"updated_at": None
|
||||
}
|
||||
|
||||
_lock = threading.Lock()
|
||||
_timer = None
|
||||
|
||||
|
||||
def _collect_once():
|
||||
data = {}
|
||||
|
||||
# shm: liste /dev/shm si disponible
|
||||
try:
|
||||
path = "/dev/shm"
|
||||
data["shm"] = os.listdir(path) if os.path.exists(path) and os.path.isdir(path) else []
|
||||
except Exception as e:
|
||||
data["shm_error"] = str(e)
|
||||
|
||||
|
||||
# cpu_info
|
||||
try:
|
||||
cpu_times = psutil.cpu_times_percent(interval=None, percpu=False)._asdict()
|
||||
data["cpu_info"] = {
|
||||
"cpu_count": psutil.cpu_count(logical=True),
|
||||
"cpu_count_physical": psutil.cpu_count(logical=False),
|
||||
"cpu_percent": psutil.cpu_percent(interval=None),
|
||||
"cpu_times_percent": cpu_times
|
||||
}
|
||||
except Exception as e:
|
||||
data["cpu_info_error"] = str(e)
|
||||
|
||||
|
||||
# memory_info
|
||||
try:
|
||||
vm = psutil.virtual_memory()._asdict()
|
||||
sm = psutil.swap_memory()._asdict()
|
||||
data["memory_info"] = {"virtual_memory": vm, "swap_memory": sm}
|
||||
except Exception as e:
|
||||
data["memory_info_error"] = str(e)
|
||||
|
||||
|
||||
# disk_info (root and partitions)
|
||||
# ex: if mountpoint == "/ramdisk" and fstype=="tmpfs" then usage.percent, usage.free, etc ...
|
||||
try:
|
||||
usage_root = psutil.disk_usage("/")._asdict()
|
||||
parts = []
|
||||
for p in psutil.disk_partitions(all=False):
|
||||
try:
|
||||
du = psutil.disk_usage(p.mountpoint)._asdict()
|
||||
except Exception:
|
||||
du = {}
|
||||
parts.append({"device": p.device, "mountpoint": p.mountpoint, "fstype": p.fstype, "usage": du})
|
||||
data["disk_info"] = {"root": usage_root, "partitions": parts}
|
||||
except Exception as e:
|
||||
data["disk_info_error"] = str(e)
|
||||
|
||||
# ramdisk
|
||||
# ex: if mountpoint == "/ramdisk" and fstype=="tmpfs" then usage.percent, usage.free, etc ...
|
||||
try:
|
||||
for part in psutil.disk_partitions(all=True):
|
||||
if part.mountpoint == RAMDISK and part.fstype.lower() == "tmpfs":
|
||||
usage = psutil.disk_usage(part.mountpoint)
|
||||
data["ramdisk_info"] = {
|
||||
"percent": usage.percent,
|
||||
"mount": part.mountpoint,
|
||||
"device": part.device,
|
||||
"fstype": part.fstype,
|
||||
"total": usage.total,
|
||||
"used": usage.used,
|
||||
"free": usage.free,
|
||||
}
|
||||
except Exception as e:
|
||||
data["ramdisk_info_error"] = str(e)
|
||||
|
||||
data["updated_at"] = time.time()
|
||||
return data
|
||||
|
||||
|
||||
def _update_cache():
|
||||
global _timer
|
||||
try:
|
||||
new = _collect_once()
|
||||
with _lock:
|
||||
_cache.update(new)
|
||||
finally:
|
||||
# reprogrammer
|
||||
_timer = threading.Timer(REFRESH_INTERVAL, _update_cache)
|
||||
_timer.daemon = True
|
||||
_timer.start()
|
||||
|
||||
|
||||
def start_background_updater(interval_seconds: int = None):
|
||||
global REFRESH_INTERVAL, _timer
|
||||
if interval_seconds:
|
||||
REFRESH_INTERVAL = interval_seconds
|
||||
if _timer is not None:
|
||||
return
|
||||
# première collecte synchronisée
|
||||
with _lock:
|
||||
_cache.update(_collect_once())
|
||||
_timer = threading.Timer(REFRESH_INTERVAL, _update_cache)
|
||||
_timer.daemon = True
|
||||
_timer.start()
|
||||
|
||||
|
||||
def stop_background_updater():
|
||||
global _timer
|
||||
if _timer is not None:
|
||||
_timer.cancel()
|
||||
_timer = None
|
||||
|
||||
|
||||
def get_cached_stats():
|
||||
with _lock:
|
||||
# retourner copie pour sécurité
|
||||
return dict(_cache)
|
||||
@@ -0,0 +1,227 @@
|
||||
'''
|
||||
Created on 20 avr. 2022
|
||||
|
||||
@author: denis
|
||||
'''
|
||||
import os
|
||||
import yaml
|
||||
import time
|
||||
import importlib
|
||||
from datetime import datetime
|
||||
import string, secrets
|
||||
import uuid
|
||||
from threading import Event, Thread
|
||||
from urllib.parse import urlsplit
|
||||
import asyncio
|
||||
import mmap
|
||||
import fcntl
|
||||
import psutil
|
||||
|
||||
SHM_DIR = "/dev/shm"
|
||||
|
||||
def open_shm(name: str, size: int, create=True):
|
||||
path = os.path.join(SHM_DIR, name)
|
||||
flags = os.O_RDWR | (os.O_CREAT if create else 0)
|
||||
fd = os.open(path, flags)
|
||||
try:
|
||||
if create:
|
||||
os.ftruncate(fd, size)
|
||||
mm = mmap.mmap(fd, size)
|
||||
finally:
|
||||
os.close(fd)
|
||||
return mm, path
|
||||
|
||||
def read_shm(name: str, size: int):
|
||||
mm, path = open_shm(name, size, create=False) # @UnusedVariable
|
||||
try:
|
||||
mm.seek(0)
|
||||
data = mm.read(size)
|
||||
return data.rstrip(b"\0")
|
||||
finally:
|
||||
mm.close()
|
||||
|
||||
def write_shm(name: str, size: int, data: bytes):
|
||||
mm, path = open_shm(name, size, create=True)
|
||||
fd = os.open(path, os.O_RDWR)
|
||||
try:
|
||||
fcntl.flock(fd, fcntl.LOCK_EX)
|
||||
try:
|
||||
mm.seek(0)
|
||||
mm.write(data.ljust(size, b"\0")[:size])
|
||||
mm.flush()
|
||||
finally:
|
||||
fcntl.flock(fd, fcntl.LOCK_UN)
|
||||
finally:
|
||||
mm.close()
|
||||
os.close(fd)
|
||||
|
||||
def get_tmpfs_info(mount_point="/ramdisk"):
|
||||
def sizeof(n):
|
||||
for unit in ['B','KB','MB','GB','TB']:
|
||||
if n < 1024:
|
||||
return f"{n:.1f}{unit}"
|
||||
n /= 1024
|
||||
return f"{n:.1f}PB"
|
||||
|
||||
usage = None
|
||||
for part in psutil.disk_partitions(all=True):
|
||||
if part.mountpoint == mount_point and part.fstype.lower() == "tmpfs":
|
||||
usage = psutil.disk_usage(part.mountpoint)
|
||||
print(f"Mount: {part.mountpoint}")
|
||||
print(f" Device: {part.device}")
|
||||
print(f" Fstype: {part.fstype}")
|
||||
print(f" Total: {usage.total} bytes ({sizeof(usage.total)})")
|
||||
print(f" Used: {usage.used} bytes ({sizeof(usage.used)})")
|
||||
print(f" Free: {usage.free} bytes ({sizeof(usage.free)})")
|
||||
print(f" Percent used: {usage.percent}%")
|
||||
break
|
||||
return {
|
||||
"percent": usage.percent,
|
||||
"mount": part.mountpoint,
|
||||
"device": part.device,
|
||||
"fstype": part.fstype,
|
||||
"total": usage.total,
|
||||
"used": usage.used,
|
||||
"free": usage.free,
|
||||
}
|
||||
|
||||
def get_cpu_info():
|
||||
# cpu percent par coeur et moyennes load
|
||||
return {
|
||||
"cpu_percent_per_cpu": psutil.cpu_percent(interval=0.5, percpu=True),
|
||||
"cpu_percent_total": psutil.cpu_percent(interval=None),
|
||||
"load_avg": os.getloadavg(), # (1,5,15)
|
||||
"cpu_count": psutil.cpu_count(logical=True),
|
||||
}
|
||||
|
||||
def get_memory_info():
|
||||
vm = psutil.virtual_memory()
|
||||
sm = psutil.swap_memory()
|
||||
return {
|
||||
"total": vm.total,
|
||||
"available": vm.available,
|
||||
"used": vm.used,
|
||||
"free": vm.free,
|
||||
"percent": vm.percent,
|
||||
"swap_total": sm.total,
|
||||
"swap_used": sm.used,
|
||||
"swap_free": sm.free,
|
||||
"swap_percent": sm.percent,
|
||||
}
|
||||
|
||||
def get_disk_info(path="/"):
|
||||
du = psutil.disk_usage(path)
|
||||
return {
|
||||
"path": path,
|
||||
"total": du.total,
|
||||
"used": du.used,
|
||||
"free": du.free,
|
||||
"percent": du.percent,
|
||||
}
|
||||
|
||||
def extract_host_port_path(url, default_port=None):
|
||||
"""
|
||||
Retoure (host, port, path) où:
|
||||
- host: string (IP ou hostname) ou None
|
||||
- port: int ou None (utilise default_port si fourni et aucun port explicite)
|
||||
- path: string (chemin + query + fragment si présents), ou '' si absent
|
||||
"""
|
||||
parts = urlsplit(url if '://' in url else '//' + url, scheme='')
|
||||
host = parts.hostname
|
||||
port = parts.port or default_port
|
||||
# Reconstruire path complet: path + ('?' + query) + ('#' + fragment)
|
||||
path = parts.path or ''
|
||||
if parts.query:
|
||||
path += '?' + parts.query
|
||||
if parts.fragment:
|
||||
path += '#' + parts.fragment
|
||||
return host, port, path
|
||||
|
||||
def image_path(imagefile):
|
||||
image_path = imagefile.path
|
||||
pdir = os.path.dirname(image_path)
|
||||
os.makedirs(pdir, exist_ok=True)
|
||||
return str(image_path)
|
||||
|
||||
def serialize_datetime(obj):
|
||||
if isinstance(obj, datetime):
|
||||
return obj.isoformat()
|
||||
raise TypeError("Type not serializable")
|
||||
|
||||
def start_async():
|
||||
loop = asyncio.new_event_loop()
|
||||
Thread(target=loop.run_forever, daemon=True).start()
|
||||
return loop
|
||||
|
||||
def stop_async(loop):
|
||||
loop.call_soon_threadsafe(loop.stop)
|
||||
|
||||
def submit_async(loop, awaitable):
|
||||
return asyncio.run_coroutine_threadsafe(awaitable, loop)
|
||||
|
||||
def to_choice(d):
|
||||
choices = []
|
||||
for k, v in d.items():
|
||||
choices.append((k, v))
|
||||
return choices
|
||||
|
||||
def get_instance_class(module):
|
||||
modulename, classname = module.rsplit(".", 1)
|
||||
return getattr(importlib.import_module(modulename), classname)
|
||||
|
||||
def wait_for(timeout):
|
||||
Event().wait(timeout)
|
||||
|
||||
def yaml_load(f):
|
||||
with open(f, 'r') as stream:
|
||||
return yaml.safe_load(stream)
|
||||
return {}
|
||||
|
||||
def yaml_save(f, context):
|
||||
with open(f, 'w') as stream:
|
||||
yaml.dump(context, stream, default_flow_style = False)
|
||||
|
||||
def get_uuid():
|
||||
return str(hex(uuid.getnode()))[2:]
|
||||
|
||||
def millis():
|
||||
return round(time.time() * 1000)
|
||||
|
||||
def now():
|
||||
return datetime.now()
|
||||
|
||||
def ts_now():
|
||||
# float second
|
||||
return now().timestamp()
|
||||
|
||||
def ts_now_s():
|
||||
return int(ts_now())
|
||||
|
||||
def ts_now_ms():
|
||||
return int(ts_now()*1000)
|
||||
|
||||
def ts_now_us():
|
||||
return int(ts_now()*1000000)
|
||||
|
||||
def random_num(n=16):
|
||||
alphabet = string.digits
|
||||
return ''.join(secrets.choice(alphabet) for i in range(n)) # @UnusedVariable
|
||||
|
||||
def random_chars(n=6):
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
return ''.join(secrets.choice(alphabet) for i in range(n)) # @UnusedVariable
|
||||
|
||||
def get_apikey(n=32):
|
||||
chars = 'abcdefgh01234ijklABCD4567EFGHIJKLmnopqrstuvwxyz0123456789MNOPQRS789TUVWXYZ'
|
||||
return ''.join(secrets.choice(chars) for i in range(n)) # @UnusedVariable
|
||||
|
||||
def gen_keywords(s):
|
||||
c = s.replace(',', ' ').replace('+', ' ')
|
||||
return [ w.strip() for w in c.split(' ') if w]
|
||||
|
||||
def gen_device_uuid(n=19):
|
||||
return hex(int(random_num(n)))[2:]
|
||||
|
||||
def get_device_uuid(n=19):
|
||||
return f'0x{gen_device_uuid(n)}'
|
||||
|
||||
Reference in New Issue
Block a user