Heavily reduced complexity to start of new
This commit is contained in:
parent
aec000de81
commit
b9cb12a2d1
29 changed files with 0 additions and 1231 deletions
19
mash.yaml
19
mash.yaml
|
@ -1,19 +0,0 @@
|
||||||
# An example config file for MaSH
|
|
||||||
|
|
||||||
services:
|
|
||||||
philips-hue:
|
|
||||||
ip: 192.168.178.42
|
|
||||||
|
|
||||||
lights:
|
|
||||||
- name: Hallway Ceiling
|
|
||||||
philips-hue: Hallway Ceiling
|
|
||||||
- name: Desk
|
|
||||||
philips-hue: Desk Plug
|
|
||||||
|
|
||||||
rooms:
|
|
||||||
- name: Office
|
|
||||||
philips-hue: Work Room
|
|
||||||
- name: Hallway
|
|
||||||
philips-hue: Hallways
|
|
||||||
adjacent:
|
|
||||||
- Office
|
|
|
@ -1,14 +1,2 @@
|
||||||
smbus2
|
|
||||||
vl53l1x
|
|
||||||
|
|
||||||
# To parse the config file
|
|
||||||
pyyaml
|
|
||||||
|
|
||||||
# To hot reload the config file
|
|
||||||
watchdog
|
|
||||||
|
|
||||||
# For Philips Hue Counter
|
# For Philips Hue Counter
|
||||||
phue
|
phue
|
||||||
|
|
||||||
# For statistics
|
|
||||||
matplotlib
|
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
import logging
|
|
||||||
|
|
||||||
from config.reloadtrigger import ReloadTrigger
|
|
||||||
|
|
||||||
try:
|
|
||||||
from watchdog.observers import Observer
|
|
||||||
from watchdog.events import (
|
|
||||||
FileSystemEventHandler,
|
|
||||||
FileCreatedEvent,
|
|
||||||
FileModifiedEvent,
|
|
||||||
)
|
|
||||||
|
|
||||||
class HotDogTrigger(ReloadTrigger, FileSystemEventHandler):
|
|
||||||
"""Trigger for config files. Might be unbound, if watchdog is not installed. Check if equal to None before use."""
|
|
||||||
|
|
||||||
def __init__(self, path: str):
|
|
||||||
super().__init__(path)
|
|
||||||
self.observer = Observer()
|
|
||||||
self.observer.schedule(self, path, recursive=True)
|
|
||||||
self.observer.start()
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
self.observer.stop()
|
|
||||||
self.observer.join()
|
|
||||||
|
|
||||||
def on_modified(self, event: FileModifiedEvent):
|
|
||||||
self.__trigger_reload()
|
|
||||||
|
|
||||||
def on_created(self, event: FileCreatedEvent):
|
|
||||||
self.__trigger_reload()
|
|
||||||
|
|
||||||
logging.debug("Watchdog imported successfully. HotDogTrigger available.")
|
|
||||||
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
logging.info("Watchdog is not installed. HotDogTrigger unavailable.")
|
|
|
@ -1,49 +0,0 @@
|
||||||
import abc
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from models.home import Home
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigParser:
|
|
||||||
def get_config(self) -> Home: # TODO: Check type
|
|
||||||
"""Load and parse the config."""
|
|
||||||
return self.__parse_config(self.__load_config())
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def __load_config(self) -> Any:
|
|
||||||
"""Load the config."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def __parse_config(self, config) -> Home:
|
|
||||||
"""Parse the config."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
|
|
||||||
class FileConfigParser(ConfigParser):
|
|
||||||
"""Scaffoling for parsers based on files."""
|
|
||||||
|
|
||||||
path: str
|
|
||||||
"""Path to the config file."""
|
|
||||||
|
|
||||||
def __init__(self, path: str):
|
|
||||||
self.path = path
|
|
||||||
|
|
||||||
|
|
||||||
class YAMLConfig(FileConfigParser):
|
|
||||||
"""Loads and parses config from a YAML file."""
|
|
||||||
|
|
||||||
def __init__(self, path: str):
|
|
||||||
super().__init__(path)
|
|
||||||
|
|
||||||
def __load_config(self) -> dict:
|
|
||||||
"""Loads config from a YAML file."""
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
with open(self.path, "r") as file:
|
|
||||||
return yaml.safe_load(file)
|
|
||||||
|
|
||||||
def __parse_config(self, config: dict) -> Home:
|
|
||||||
"""Parses config in dict format. Usually based on JSON or YAML."""
|
|
||||||
# TODO: Implement
|
|
||||||
raise NotImplementedError()
|
|
|
@ -1,26 +0,0 @@
|
||||||
class ReloadTrigger(object):
|
|
||||||
"""Abstract interface for config hot reloading."""
|
|
||||||
|
|
||||||
path: str
|
|
||||||
"""Path to the config file."""
|
|
||||||
|
|
||||||
callbacks: list
|
|
||||||
"""List of functions to callback on reload of config."""
|
|
||||||
|
|
||||||
def __init__(self, path: str):
|
|
||||||
"""Initialize the trigger.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
path (str): Path to the config file.
|
|
||||||
"""
|
|
||||||
self.path = path
|
|
||||||
self.callbacks = []
|
|
||||||
|
|
||||||
def on_reload(self, callback):
|
|
||||||
"""Register a callback to be called when the config is reloaded."""
|
|
||||||
self.callbacks.append(callback)
|
|
||||||
|
|
||||||
def __trigger_reload(self):
|
|
||||||
"""Trigger a reload of the config."""
|
|
||||||
for callback in self.callbacks:
|
|
||||||
callback()
|
|
0
src/main.py
Normal file
0
src/main.py
Normal file
48
src/mash.py
48
src/mash.py
|
@ -1,48 +0,0 @@
|
||||||
from config.parser import ConfigParser
|
|
||||||
from config.reloadtrigger import ReloadTrigger
|
|
||||||
from models.home import Home
|
|
||||||
|
|
||||||
|
|
||||||
class MaSH:
|
|
||||||
"""Max' Smart Home. Smart home automation framework."""
|
|
||||||
|
|
||||||
config_parser: ConfigParser
|
|
||||||
"""Parser for the config file."""
|
|
||||||
|
|
||||||
config_trigger: list[ReloadTrigger]
|
|
||||||
"""Active triggers for reloading the config."""
|
|
||||||
|
|
||||||
home: Home
|
|
||||||
"""Home to control and make smart."""
|
|
||||||
|
|
||||||
def __init__(self, config_parser: ConfigParser) -> None:
|
|
||||||
"""Initialize the framework.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_parser (ConfigParser): Parser for the config file.
|
|
||||||
"""
|
|
||||||
self.config_parser = config_parser
|
|
||||||
self.config_trigger = []
|
|
||||||
|
|
||||||
self.reload_config()
|
|
||||||
|
|
||||||
def reload_config(self) -> None:
|
|
||||||
"""Reload the config."""
|
|
||||||
self.home = self.config_parser.get_config() # TODO: Check type
|
|
||||||
|
|
||||||
def register_config_trigger(self, trigger: ReloadTrigger) -> bool:
|
|
||||||
"""Register a trigger for hot-reloading the config.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
trigger (ReloadTrigger): Trigger for hot-reloading the config based on ReloadTrigger.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if the trigger was registered, False if not.
|
|
||||||
"""
|
|
||||||
if trigger is None or trigger in self.config_trigger:
|
|
||||||
return False
|
|
||||||
|
|
||||||
trigger.on_reload(self.reload_config)
|
|
||||||
self.config_trigger.append(trigger)
|
|
||||||
|
|
||||||
return True
|
|
|
@ -1,23 +0,0 @@
|
||||||
class RGBColor:
|
|
||||||
"""Represents a color in the RGB color space."""
|
|
||||||
|
|
||||||
r: int
|
|
||||||
"""Red value of the color."""
|
|
||||||
|
|
||||||
g: int
|
|
||||||
"""Green value of the color."""
|
|
||||||
|
|
||||||
b: int
|
|
||||||
"""Blue value of the color."""
|
|
||||||
|
|
||||||
def __init__(self, r: int, g: int, b: int):
|
|
||||||
"""Initialize the color.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
r (int): Red value of the color.
|
|
||||||
g (int): Green value of the color.
|
|
||||||
b (int): Blue value of the color.
|
|
||||||
"""
|
|
||||||
self.r = r
|
|
||||||
self.g = g
|
|
||||||
self.b = b
|
|
|
@ -1,9 +0,0 @@
|
||||||
from models.devices.colordevices import *
|
|
||||||
from models.devices.switchdevices import *
|
|
||||||
from models.devices.genericdevices import *
|
|
||||||
|
|
||||||
|
|
||||||
class AllDevice(SetSwitchDevice, SetColorDevice):
|
|
||||||
"""Inherits all intefaces from all devices."""
|
|
||||||
|
|
||||||
pass
|
|
|
@ -1,25 +0,0 @@
|
||||||
from models.colors import RGBColor
|
|
||||||
from models.devices.genericdevices import GenericDevice
|
|
||||||
|
|
||||||
|
|
||||||
class ColorDevice(GenericDevice):
|
|
||||||
"""Abstract device that has a color."""
|
|
||||||
|
|
||||||
_color: RGBColor
|
|
||||||
"""Color of device."""
|
|
||||||
|
|
||||||
|
|
||||||
class GetColorDevice(ColorDevice):
|
|
||||||
"""Implement color getter for a color device."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def color(self) -> RGBColor:
|
|
||||||
return self._color
|
|
||||||
|
|
||||||
|
|
||||||
class SetColorDevice(GetColorDevice):
|
|
||||||
"""Implements color setter for a color device."""
|
|
||||||
|
|
||||||
@GetColorDevice.color.setter
|
|
||||||
def color(self, color: RGBColor):
|
|
||||||
self._color = color
|
|
|
@ -1,5 +0,0 @@
|
||||||
class GenericDevice:
|
|
||||||
"""A generic device."""
|
|
||||||
|
|
||||||
def __init__(self, name: str):
|
|
||||||
self.name = name
|
|
|
@ -1,43 +0,0 @@
|
||||||
from models.devices.genericdevices import GenericDevice
|
|
||||||
|
|
||||||
|
|
||||||
class SwitchDevice(GenericDevice):
|
|
||||||
"""Abstract device that can be turned on and off."""
|
|
||||||
|
|
||||||
_is_on: bool
|
|
||||||
"""Current state of the device."""
|
|
||||||
|
|
||||||
|
|
||||||
class ToggleSwitchDevice(SwitchDevice):
|
|
||||||
"""Implements toggle functionality for a switch device."""
|
|
||||||
|
|
||||||
def toggle(self):
|
|
||||||
self._is_on = not self._is_on
|
|
||||||
|
|
||||||
|
|
||||||
class TurnOffSwitchDevice(SwitchDevice):
|
|
||||||
"""Implements turn off functionality for a switch device."""
|
|
||||||
|
|
||||||
def turn_off(self):
|
|
||||||
self._is_on = False
|
|
||||||
|
|
||||||
|
|
||||||
class TurnOnSwitchDevice(SwitchDevice):
|
|
||||||
"""Implements turn on functionality for a switch device."""
|
|
||||||
|
|
||||||
def turn_on(self):
|
|
||||||
self._is_on = True
|
|
||||||
|
|
||||||
|
|
||||||
class GetSwitchDevice(SwitchDevice):
|
|
||||||
"""Implements is_on state getter for a switch device."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_on(self) -> bool:
|
|
||||||
return self._is_on
|
|
||||||
|
|
||||||
|
|
||||||
class SetSwitchDevice(TurnOffSwitchDevice, TurnOnSwitchDevice, ToggleSwitchDevice):
|
|
||||||
@GetSwitchDevice.is_on.setter
|
|
||||||
def is_on(self, set_on: bool):
|
|
||||||
self._is_on = set_on
|
|
|
@ -1,2 +0,0 @@
|
||||||
class NoDeviceFoundError(Exception):
|
|
||||||
pass
|
|
|
@ -1,71 +0,0 @@
|
||||||
from models.helper import filter_devices
|
|
||||||
from models.devices import AllDevice
|
|
||||||
from models.colors import RGBColor
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceGroup(AllDevice):
|
|
||||||
"""A group of devices that allows group operations. Inherits from a complex device to offer group operations for all devices."""
|
|
||||||
|
|
||||||
def __init__(self, devices: list[GenericDevice]):
|
|
||||||
self._devices = devices
|
|
||||||
|
|
||||||
@property
|
|
||||||
def devices(self) -> list[GenericDevice]:
|
|
||||||
return self._devices
|
|
||||||
|
|
||||||
@property
|
|
||||||
def switches(self) -> list[SwitchDevice]:
|
|
||||||
return filter_devices(self.devices, SwitchDevice)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def lights(self) -> list[LightDevice]:
|
|
||||||
return filter_devices(self.devices, LightDevice)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_on(self) -> bool:
|
|
||||||
"""Returns true if any device is on."""
|
|
||||||
return any(device.is_on for device in self.switches)
|
|
||||||
|
|
||||||
@is_on.setter
|
|
||||||
def is_on(self, is_on: bool):
|
|
||||||
"""Sets all devices to the same state."""
|
|
||||||
for device in self.switches:
|
|
||||||
device.is_on = is_on
|
|
||||||
|
|
||||||
@property
|
|
||||||
def color(self) -> LightColor:
|
|
||||||
"""Returns the color of the first light in the group."""
|
|
||||||
return self.lights[0].color
|
|
||||||
|
|
||||||
@color.setter
|
|
||||||
def color(self, color: LightColor):
|
|
||||||
"""Sets all lights in the group to the same color."""
|
|
||||||
for device in self.lights:
|
|
||||||
device.color = color
|
|
||||||
|
|
||||||
@property
|
|
||||||
def scene(self) -> LightScene:
|
|
||||||
"""Returns the current scene of the group."""
|
|
||||||
return LightScene(self.name, {device: device.color for device in self.lights})
|
|
||||||
|
|
||||||
|
|
||||||
class Room(DeviceGroup):
|
|
||||||
"""A group of devices that has additional properties a can be seen as a room."""
|
|
||||||
|
|
||||||
def __init__(self, name: str, devices: list[GenericDevice], people_count: int = 0):
|
|
||||||
"""
|
|
||||||
Args:
|
|
||||||
name (str): Name of the room.
|
|
||||||
devices (list[GenericDevice]): Devices in this room.
|
|
||||||
people_count (int, optional): Number of people in this room. Defaults to 0.
|
|
||||||
"""
|
|
||||||
super().__init__(devices)
|
|
||||||
self.name = name
|
|
||||||
self._people_count = people_count
|
|
||||||
|
|
||||||
@property
|
|
||||||
def people_count(self) -> int:
|
|
||||||
return self._people_count
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
|
@ -1,28 +0,0 @@
|
||||||
from typing import TypeVar
|
|
||||||
from models.devices.genericdevices import GenericDevice, LightDevice, SwitchDevice
|
|
||||||
from models.exceptions import NoDeviceFoundError
|
|
||||||
from models.groups import DeviceGroup, Room
|
|
||||||
|
|
||||||
|
|
||||||
DEVICE_TYPE = TypeVar(
|
|
||||||
"DEVICE_TYPE",
|
|
||||||
type(GenericDevice),
|
|
||||||
type(SwitchDevice),
|
|
||||||
type(LightDevice),
|
|
||||||
type(Room),
|
|
||||||
type(DeviceGroup),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def filter_devices(
|
|
||||||
devices: list[GenericDevice], type: DEVICE_TYPE
|
|
||||||
) -> list[DEVICE_TYPE]:
|
|
||||||
"""Filters out devices that are not of a specific type."""
|
|
||||||
filtered_devices: list[DEVICE_TYPE] = [
|
|
||||||
device for device in devices if isinstance(device, type)
|
|
||||||
]
|
|
||||||
|
|
||||||
if len(filtered_devices) == 0:
|
|
||||||
raise NoDeviceFoundError(f"No devices of type {type} found.")
|
|
||||||
|
|
||||||
return filtered_devices
|
|
|
@ -1,19 +0,0 @@
|
||||||
from models.groups import DeviceGroup, Room
|
|
||||||
from models.helper import filter_devices
|
|
||||||
|
|
||||||
|
|
||||||
class Home(Room):
|
|
||||||
"""Combines all elements of a smart home."""
|
|
||||||
|
|
||||||
location: str
|
|
||||||
"""Physical location of the home, useful for sunset and sunrise times."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def rooms(self) -> list[Room]:
|
|
||||||
"""Returns all rooms in the home."""
|
|
||||||
return filter_devices(self.devices, Room)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def groups(self) -> list[DeviceGroup]:
|
|
||||||
"""Returns all groups in the home."""
|
|
||||||
return filter_devices(self.devices, DeviceGroup)
|
|
|
@ -1,283 +0,0 @@
|
||||||
from datetime import datetime, time, timedelta
|
|
||||||
from typing import Dict
|
|
||||||
from old.philips_hue import PhilipsHue
|
|
||||||
from sensors import PeopleCounter, Directions, VL53L1XSensor
|
|
||||||
import logging
|
|
||||||
import json
|
|
||||||
from timeloop import Timeloop
|
|
||||||
|
|
||||||
|
|
||||||
# Should lights already turn on where there is any kind of motion in the sensor
|
|
||||||
ENABLE_MOTION_TRIGGERED_LIGHT = True
|
|
||||||
|
|
||||||
# Should lights change when a certain time in the schedule is reached
|
|
||||||
ENABLE_SCHEDULE_TRIGGERS = (
|
|
||||||
False # Not working correctly at the moment, so turned off by default
|
|
||||||
)
|
|
||||||
|
|
||||||
# Schedule (Key is time after scene should be used. Value is scene name to be used.)
|
|
||||||
# Needs to be sorted chronologically
|
|
||||||
SCHEDULE = {}
|
|
||||||
|
|
||||||
|
|
||||||
LOG_FILE_PATH = "log.txt" # Path for logs
|
|
||||||
hue_conf = {
|
|
||||||
"bridge_ip": "",
|
|
||||||
"transition_time": 10, # seconds
|
|
||||||
"light_group": "",
|
|
||||||
# If file exists, application is considered 'registered' at the bridge
|
|
||||||
"registered_file": "smart_light_registered.bridge",
|
|
||||||
} # Custom configuration for philips hue
|
|
||||||
|
|
||||||
|
|
||||||
hue: PhilipsHue = PhilipsHue(hue_conf) # Light interface
|
|
||||||
counter: PeopleCounter = PeopleCounter(VL53L1XSensor()) # Sensor object
|
|
||||||
peopleCount: int = 0 # Global count of people on the inside
|
|
||||||
motion_triggered_lights = False # Is light on because of any detected motion
|
|
||||||
timeloop: Timeloop = Timeloop() # Used for time triggered schedule
|
|
||||||
|
|
||||||
logging.getLogger().setLevel(logging.INFO)
|
|
||||||
|
|
||||||
|
|
||||||
def time_minus_time(time_a: time, time_b: time) -> timedelta:
|
|
||||||
"""Implementes a basic timedelta function for time objects.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
time_a (time): Time to subtract from.
|
|
||||||
time_b (time): Time to be subtracted.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
timedelta: Delta between the two time objects.
|
|
||||||
"""
|
|
||||||
today = datetime.today()
|
|
||||||
dt_a = datetime.combine(today, time_a)
|
|
||||||
dt_b = datetime.combine(today, time_b)
|
|
||||||
|
|
||||||
return dt_a - dt_b
|
|
||||||
|
|
||||||
|
|
||||||
def get_scene_for_time(time: time) -> str:
|
|
||||||
"""Determines the correct scene to activate for a given time.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
time (time): Time to find scene for.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
string: Scene name that should be active. None, if schedule is empty.
|
|
||||||
"""
|
|
||||||
global SCHEDULE
|
|
||||||
|
|
||||||
if SCHEDULE is None or len(SCHEDULE) <= 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
previous_scene = None
|
|
||||||
for start_time, scene in SCHEDULE.items():
|
|
||||||
# If current time is still after schedule time, just keep going
|
|
||||||
if start_time <= time:
|
|
||||||
previous_scene = scene
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Schedule timef is now after current time, which is too late
|
|
||||||
# So if exists, take previous scene, since it was the last before the current time
|
|
||||||
if previous_scene:
|
|
||||||
return previous_scene
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Only breaks if it could not find a valid scene, so use lates scene as fallback
|
|
||||||
return list(SCHEDULE.values())[-1]
|
|
||||||
|
|
||||||
|
|
||||||
def change_cb(countChange: int, directionState: Dict):
|
|
||||||
"""Handles basic logging of event data for later analysis.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
countChange (int): The change in the number of people. Usually on of [-1, 0, 1].
|
|
||||||
directionState (Dict): Object describing the internal state of the sensor.
|
|
||||||
"""
|
|
||||||
data = {
|
|
||||||
"version": "v0.0",
|
|
||||||
"previousPeopleCount": peopleCount,
|
|
||||||
"countChange": countChange,
|
|
||||||
"directionState": directionState,
|
|
||||||
"dateTime": datetime.now(),
|
|
||||||
"motionTriggeredLights": motion_triggered_lights,
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(LOG_FILE_PATH, "a") as f:
|
|
||||||
f.write(json.dumps(data, default=str) + "\n")
|
|
||||||
except Exception as ex:
|
|
||||||
logging.exception(f"Unable to write log file. {ex}")
|
|
||||||
|
|
||||||
|
|
||||||
def count_change(change: int) -> None:
|
|
||||||
"""Handles light state when people count changes
|
|
||||||
|
|
||||||
Args:
|
|
||||||
change (int): The change in the number of people. Usually on of [-1, 0, 1].
|
|
||||||
"""
|
|
||||||
global hue
|
|
||||||
global peopleCount
|
|
||||||
global motion_triggered_lights
|
|
||||||
|
|
||||||
# Are lights on at the moment?
|
|
||||||
previous_lights_state = get_light_state()
|
|
||||||
|
|
||||||
# Apply correction
|
|
||||||
if peopleCount <= 0 and previous_lights_state and not motion_triggered_lights:
|
|
||||||
# Count was 0, but lights were on (not because of motion triggers) => people count was not actually 0
|
|
||||||
peopleCount = 1
|
|
||||||
logging.debug(f"People count corrected to {peopleCount}")
|
|
||||||
elif peopleCount > 0 and not previous_lights_state:
|
|
||||||
# Count was >0, but lights were off => people count was actually 0
|
|
||||||
peopleCount = 0
|
|
||||||
logging.debug(f"People count corrected to {peopleCount}")
|
|
||||||
|
|
||||||
peopleCount += change
|
|
||||||
if peopleCount < 0:
|
|
||||||
peopleCount = 0
|
|
||||||
logging.debug(f"People count changed by {change}")
|
|
||||||
|
|
||||||
# Handle light
|
|
||||||
target_light_state = peopleCount > 0
|
|
||||||
|
|
||||||
# Return, if there is no change
|
|
||||||
if previous_lights_state == target_light_state:
|
|
||||||
if previous_lights_state:
|
|
||||||
# Signaling that the people count is taking control over the light now
|
|
||||||
motion_triggered_lights = False
|
|
||||||
return
|
|
||||||
|
|
||||||
set_light_state(target_light_state)
|
|
||||||
|
|
||||||
|
|
||||||
def trigger_change(triggerState: Dict):
|
|
||||||
"""Handles motion triggered light state.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
triggerState (Dict): Describing in what directions the sensor is triggerd.
|
|
||||||
"""
|
|
||||||
global hue
|
|
||||||
global motion_triggered_lights
|
|
||||||
|
|
||||||
target_light_state = None
|
|
||||||
|
|
||||||
# Is someone walking close to the door?
|
|
||||||
motion_detected = (
|
|
||||||
triggerState[Directions.INSIDE] or triggerState[Directions.OUTSIDE]
|
|
||||||
)
|
|
||||||
target_light_state = motion_detected
|
|
||||||
|
|
||||||
# Does motion triggered light need to do anything?
|
|
||||||
if peopleCount > 0:
|
|
||||||
# State is successfully handled by the count
|
|
||||||
motion_triggered_lights = False
|
|
||||||
return
|
|
||||||
|
|
||||||
# Only look at changing situations
|
|
||||||
if target_light_state == motion_triggered_lights:
|
|
||||||
return
|
|
||||||
|
|
||||||
set_light_state(target_light_state)
|
|
||||||
|
|
||||||
# Save state
|
|
||||||
motion_triggered_lights = target_light_state
|
|
||||||
|
|
||||||
|
|
||||||
def set_light_scene(target_scene: str) -> bool:
|
|
||||||
"""Sets the lights to the given scene, but only, if lights are already on. Does not correct count if lights are in an unexpected state.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
target_scene (string): Name of the scene to activate.
|
|
||||||
"""
|
|
||||||
# Is valid scene?
|
|
||||||
if target_scene is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Are lights on at the moment? Only based on people count for simplicity
|
|
||||||
if peopleCount <= 0:
|
|
||||||
# Lights are probably off, not doing anything
|
|
||||||
return
|
|
||||||
|
|
||||||
# Set lights to scene
|
|
||||||
hue.set_group_scene(hue_conf["light_group"], target_scene)
|
|
||||||
logging.debug(f"Light scene set to {target_scene}")
|
|
||||||
|
|
||||||
|
|
||||||
def set_light_state(target_light_state: bool) -> bool:
|
|
||||||
"""Sets the lights to the given state.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
target_light_state (bool): Should lights on the inside be on or off.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: Previous light state.
|
|
||||||
"""
|
|
||||||
# Are lights on at the moment?
|
|
||||||
previous_lights_state = get_light_state()
|
|
||||||
if target_light_state == previous_lights_state:
|
|
||||||
return previous_lights_state
|
|
||||||
|
|
||||||
# Adjust light as necessary
|
|
||||||
target_scene = get_scene_for_time(datetime.now().time())
|
|
||||||
if target_light_state and target_scene:
|
|
||||||
# Set to specific scene if exists
|
|
||||||
hue.set_group_scene(hue_conf["light_group"], target_scene)
|
|
||||||
logging.debug(
|
|
||||||
f"Light state changed to {target_light_state} with scene {target_scene}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
hue.set_group(hue_conf["light_group"], {"on": target_light_state})
|
|
||||||
logging.debug(f"Light state changed to {target_light_state}")
|
|
||||||
|
|
||||||
return previous_lights_state
|
|
||||||
|
|
||||||
|
|
||||||
def get_light_state() -> bool:
|
|
||||||
"""
|
|
||||||
Returns:
|
|
||||||
bool: Current light state.
|
|
||||||
"""
|
|
||||||
return hue.get_group(hue_conf["light_group"])["state"]["any_on"]
|
|
||||||
|
|
||||||
|
|
||||||
def update_scene():
|
|
||||||
"""Called by time trigger to update light scene if lights are on."""
|
|
||||||
scene = get_scene_for_time(datetime.now().time())
|
|
||||||
|
|
||||||
if scene is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
set_light_scene(scene)
|
|
||||||
logging.debug(f"Updated scene at {datetime.now().time()} to {scene}.")
|
|
||||||
|
|
||||||
|
|
||||||
def register_time_triggers():
|
|
||||||
"""Registeres time triggered callbacks based on the schedule, to adjust the current scene, if lights are on."""
|
|
||||||
global SCHEDULE
|
|
||||||
if SCHEDULE is None or len(SCHEDULE) <= 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
for time in SCHEDULE.keys():
|
|
||||||
delta = time_minus_time(time, datetime.now().time())
|
|
||||||
if delta < timedelta(0):
|
|
||||||
delta += timedelta(1)
|
|
||||||
|
|
||||||
timeloop._add_job(update_scene, interval=timedelta(1), offset=delta)
|
|
||||||
|
|
||||||
timeloop.start(block=False)
|
|
||||||
|
|
||||||
logging.info("Registered time triggers.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
if ENABLE_SCHEDULE_TRIGGERS:
|
|
||||||
register_time_triggers()
|
|
||||||
|
|
||||||
# Represents callback trigger order
|
|
||||||
counter.hookChange(change_cb)
|
|
||||||
counter.hookCounting(count_change)
|
|
||||||
counter.hookTrigger(trigger_change)
|
|
||||||
|
|
||||||
counter.run()
|
|
|
@ -1,3 +0,0 @@
|
||||||
from sensors.vl53l1x_sensor import VL53L1XSensor
|
|
||||||
from sensors.people_counter import PeopleCounter
|
|
||||||
from sensors.tof_sensor import Directions
|
|
|
@ -1,195 +0,0 @@
|
||||||
from sensors.tof_sensor import ToFSensor, Directions
|
|
||||||
from datetime import datetime
|
|
||||||
import threading
|
|
||||||
|
|
||||||
|
|
||||||
COUNTING_CB = "counting"
|
|
||||||
TRIGGER_CB = "trigger"
|
|
||||||
CHANGE_CB = "changes"
|
|
||||||
START_TIME = "start_time"
|
|
||||||
END_TIME = "end_time"
|
|
||||||
TRIGGER_DISTANCES = "trigger_distances"
|
|
||||||
END_DISTANCE = "end_distance"
|
|
||||||
|
|
||||||
|
|
||||||
class PeopleCounter:
|
|
||||||
def __init__(self, sensor: ToFSensor) -> None:
|
|
||||||
self.sensor = sensor
|
|
||||||
self.callbacks = {COUNTING_CB: [], TRIGGER_CB: [], CHANGE_CB: []}
|
|
||||||
self.maxTriggerDistance = 120 # In cm
|
|
||||||
|
|
||||||
def hookCounting(self, cb) -> None:
|
|
||||||
self.callbacks[COUNTING_CB].append(cb)
|
|
||||||
|
|
||||||
def unhookCounting(self, cb) -> None:
|
|
||||||
self.callbacks[COUNTING_CB].remove(cb)
|
|
||||||
|
|
||||||
def hookTrigger(self, cb) -> None:
|
|
||||||
self.callbacks[TRIGGER_CB].append(cb)
|
|
||||||
|
|
||||||
def unhookTrigger(self, cb) -> None:
|
|
||||||
self.callbacks[TRIGGER_CB].remove(cb)
|
|
||||||
|
|
||||||
def hookChange(self, cb) -> None:
|
|
||||||
self.callbacks[CHANGE_CB].append(cb)
|
|
||||||
|
|
||||||
def unhookChange(self, cb) -> None:
|
|
||||||
self.callbacks[CHANGE_CB].remove(cb)
|
|
||||||
|
|
||||||
def getInitialDirectionState(self) -> dict[Directions, list]:
|
|
||||||
return {Directions.INSIDE: [], Directions.OUTSIDE: []}
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
self.keepRunning = True
|
|
||||||
direction = Directions.INSIDE
|
|
||||||
self.directionState = self.getInitialDirectionState()
|
|
||||||
|
|
||||||
self.sensor.open()
|
|
||||||
while self.keepRunning:
|
|
||||||
# Switch to other direction
|
|
||||||
direction: Directions = Directions.other(direction)
|
|
||||||
|
|
||||||
self.sensor.setDirection(direction)
|
|
||||||
|
|
||||||
distance: float = self.sensor.getDistance()
|
|
||||||
changed: bool = self.updateState(direction, distance)
|
|
||||||
|
|
||||||
if changed:
|
|
||||||
countChange: int = self.getCountChange(self.directionState)
|
|
||||||
|
|
||||||
# Hooks
|
|
||||||
th = threading.Thread(target=self.handleCallbacks, args=(countChange,))
|
|
||||||
th.start()
|
|
||||||
|
|
||||||
# Reset state if state is finalised
|
|
||||||
if not self.isDirectionTriggered(
|
|
||||||
Directions.INSIDE
|
|
||||||
) and not self.isDirectionTriggered(Directions.OUTSIDE):
|
|
||||||
self.directionState = self.getInitialDirectionState()
|
|
||||||
|
|
||||||
self.sensor.close()
|
|
||||||
|
|
||||||
def getCountChange(self, directionState) -> int:
|
|
||||||
# Is valid?
|
|
||||||
for direction in Directions:
|
|
||||||
# Is there at least one record for every direction?
|
|
||||||
if len(directionState[direction]) <= 0:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# Did every record start and end?
|
|
||||||
if (
|
|
||||||
directionState[direction][0][START_TIME] is None
|
|
||||||
or directionState[direction][-1][END_TIME] is None
|
|
||||||
):
|
|
||||||
return 0 # Return no change if not valid
|
|
||||||
|
|
||||||
# Get times into variables
|
|
||||||
insideStart = directionState[Directions.INSIDE][0][START_TIME]
|
|
||||||
insideEnd = directionState[Directions.INSIDE][-1][END_TIME]
|
|
||||||
outsideStart = directionState[Directions.OUTSIDE][0][START_TIME]
|
|
||||||
outsideEnd = directionState[Directions.OUTSIDE][-1][END_TIME]
|
|
||||||
|
|
||||||
# In what direction is the doorframe entered and left?
|
|
||||||
# Entering doorframe in the inside direction
|
|
||||||
enteringInside: bool = outsideStart < insideStart
|
|
||||||
# Leaving dooframe in the inside direction
|
|
||||||
leavingInside: bool = outsideEnd < insideEnd
|
|
||||||
|
|
||||||
# They have to be the same, otherwise they switch directions in between
|
|
||||||
if enteringInside != leavingInside:
|
|
||||||
# Someone did not go all the way
|
|
||||||
# Either
|
|
||||||
# Inside -######-
|
|
||||||
# Outside ---##---
|
|
||||||
# or
|
|
||||||
# Inside ---##---
|
|
||||||
# Outside -######-
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# Are those times overlapping or disjunct?
|
|
||||||
if insideEnd < outsideStart or outsideEnd < insideStart:
|
|
||||||
# They are disjunct
|
|
||||||
# Either
|
|
||||||
# Inside -##-----
|
|
||||||
# Outside -----##-
|
|
||||||
# or
|
|
||||||
# Inside -----##-
|
|
||||||
# Outside -##-----
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# What direction is the person taking?
|
|
||||||
if enteringInside:
|
|
||||||
# Entering the inside
|
|
||||||
# Inside ---####-
|
|
||||||
# Outside -####---
|
|
||||||
return 1
|
|
||||||
else:
|
|
||||||
# Leaving the inside
|
|
||||||
# Inside -####---
|
|
||||||
# Outside ---####-
|
|
||||||
return -1
|
|
||||||
|
|
||||||
def isTriggerDistance(self, distance: float) -> bool:
|
|
||||||
#! TODO: Should be based on the distance from the ground, not from the sensor
|
|
||||||
return distance <= self.maxTriggerDistance
|
|
||||||
|
|
||||||
def handleCallbacks(self, countChange: int):
|
|
||||||
self.handleChangeCallbacks(countChange)
|
|
||||||
self.handleCountingCallbacks(countChange)
|
|
||||||
self.handleTriggerCallbacks()
|
|
||||||
|
|
||||||
def handleCountingCallbacks(self, countChange: int) -> None:
|
|
||||||
# Only notify counting on actual count change
|
|
||||||
if countChange == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
for cb in self.callbacks[COUNTING_CB]:
|
|
||||||
cb(countChange)
|
|
||||||
|
|
||||||
def handleTriggerCallbacks(self) -> None:
|
|
||||||
triggerState = {
|
|
||||||
Directions.INSIDE: self.isDirectionTriggered(Directions.INSIDE),
|
|
||||||
Directions.OUTSIDE: self.isDirectionTriggered(Directions.OUTSIDE),
|
|
||||||
}
|
|
||||||
|
|
||||||
for cb in self.callbacks[TRIGGER_CB]:
|
|
||||||
cb(triggerState)
|
|
||||||
|
|
||||||
def handleChangeCallbacks(self, countChange: int) -> None:
|
|
||||||
for cb in self.callbacks[CHANGE_CB]:
|
|
||||||
cb(countChange, self.directionState)
|
|
||||||
|
|
||||||
def isDirectionTriggered(self, direction: Directions) -> bool:
|
|
||||||
return (
|
|
||||||
len(self.directionState[direction]) > 0
|
|
||||||
and self.directionState[direction][-1][END_TIME] is None
|
|
||||||
)
|
|
||||||
|
|
||||||
def updateState(self, direction: Directions, distance: float) -> bool:
|
|
||||||
triggered: bool = self.isTriggerDistance(distance)
|
|
||||||
|
|
||||||
previouslyTriggered = False
|
|
||||||
if len(self.directionState[direction]) > 0:
|
|
||||||
previouslyTriggered = self.directionState[direction][-1][END_TIME] is None
|
|
||||||
|
|
||||||
if triggered and not previouslyTriggered:
|
|
||||||
# Set as new beginning for this direction
|
|
||||||
self.directionState[direction].append(
|
|
||||||
{
|
|
||||||
START_TIME: datetime.now(),
|
|
||||||
END_TIME: None,
|
|
||||||
TRIGGER_DISTANCES: [distance],
|
|
||||||
END_DISTANCE: None,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
elif not triggered and previouslyTriggered:
|
|
||||||
# Set as end for this direction
|
|
||||||
self.directionState[direction][-1][END_TIME] = datetime.now()
|
|
||||||
self.directionState[direction][-1][END_DISTANCE] = distance
|
|
||||||
return True
|
|
||||||
elif previouslyTriggered:
|
|
||||||
# Add distance at least
|
|
||||||
self.directionState[direction][-1][TRIGGER_DISTANCES].append(distance)
|
|
||||||
|
|
||||||
return False
|
|
|
@ -1,31 +0,0 @@
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class Directions(str, Enum):
|
|
||||||
INSIDE = "indoor"
|
|
||||||
OUTSIDE = "outdoor"
|
|
||||||
|
|
||||||
def other(direction: "Directions") -> "Directions":
|
|
||||||
if direction is Directions.INSIDE:
|
|
||||||
return Directions.OUTSIDE
|
|
||||||
else:
|
|
||||||
return Directions.INSIDE
|
|
||||||
|
|
||||||
def __iter__():
|
|
||||||
return [Directions.INSIDE, Directions.OUTSIDE]
|
|
||||||
|
|
||||||
|
|
||||||
class ToFSensor:
|
|
||||||
def open(self) -> None:
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def setDirection(self, direction: Directions) -> None:
|
|
||||||
"""Configure sensor to pick up the distance in a specific direction."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def getDistance(self) -> float:
|
|
||||||
"""Returns new distance in cm."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
raise NotImplementedError()
|
|
|
@ -1,62 +0,0 @@
|
||||||
from sensors.tof_sensor import Directions, ToFSensor
|
|
||||||
import VL53L1X
|
|
||||||
|
|
||||||
# Reference: https://github.com/pimoroni/vl53l1x-python
|
|
||||||
#
|
|
||||||
# Left, right, top and bottom are relative to the SPAD matrix coordinates,
|
|
||||||
# which will be mirrored in real scene coordinates.
|
|
||||||
# (or even rotated, depending on the VM53L1X element alignment on the board and on the board position)
|
|
||||||
#
|
|
||||||
# ROI in SPAD matrix coords:
|
|
||||||
#
|
|
||||||
# 15 top-left
|
|
||||||
# | X____
|
|
||||||
# | | |
|
|
||||||
# | |____X
|
|
||||||
# | bottom-right
|
|
||||||
# 0__________15
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
class VL53L1XSensor(ToFSensor):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
def open(self) -> None:
|
|
||||||
self.sensor = VL53L1X.VL53L1X(i2c_bus=1, i2c_address=0x29)
|
|
||||||
self.sensor.open()
|
|
||||||
|
|
||||||
# Optionally set an explicit timing budget
|
|
||||||
# These values are measurement time in microseconds,
|
|
||||||
# and inter-measurement time in milliseconds.
|
|
||||||
# If you uncomment the line below to set a budget you
|
|
||||||
# should use `tof.start_ranging(0)`
|
|
||||||
# tof.set_timing(66000, 70)
|
|
||||||
self.ranging = 2
|
|
||||||
# 0 = Unchanged
|
|
||||||
# 1 = Short Range
|
|
||||||
# 2 = Medium Range
|
|
||||||
# 3 = Long Range
|
|
||||||
|
|
||||||
def setDirection(self, direction: Directions) -> None:
|
|
||||||
"""Configure sensor to pick up the distance in a specific direction."""
|
|
||||||
direction_roi = {
|
|
||||||
Directions.INSIDE: VL53L1X.VL53L1xUserRoi(6, 3, 9, 0),
|
|
||||||
Directions.OUTSIDE: VL53L1X.VL53L1xUserRoi(6, 15, 9, 12),
|
|
||||||
}
|
|
||||||
|
|
||||||
roi = direction_roi[direction]
|
|
||||||
|
|
||||||
self.sensor.stop_ranging()
|
|
||||||
self.sensor.set_user_roi(roi)
|
|
||||||
self.sensor.start_ranging(self.ranging)
|
|
||||||
|
|
||||||
def getDistance(self) -> float:
|
|
||||||
"""Returns new distance in cm."""
|
|
||||||
distance = self.sensor.get_distance()
|
|
||||||
|
|
||||||
return distance / 10
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
self.sensor.stop_ranging()
|
|
||||||
self.sensor.close()
|
|
|
@ -1,112 +0,0 @@
|
||||||
from datetime import datetime
|
|
||||||
import json
|
|
||||||
from typing import Dict
|
|
||||||
from xmlrpc.client import Boolean
|
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
|
|
||||||
# Config
|
|
||||||
FILE_PATH = "log.txt"
|
|
||||||
AFTER_DATE = datetime(2022, 1, 1) # Only keeps log entries after the specified date. Filter applied in parse_log_entry(..)
|
|
||||||
|
|
||||||
|
|
||||||
# Read file
|
|
||||||
content = None
|
|
||||||
with open(FILE_PATH, "r") as file:
|
|
||||||
content = file.readlines()
|
|
||||||
|
|
||||||
|
|
||||||
def parse_log_entry(entry: Dict) -> Dict | bool:
|
|
||||||
# Only keep last record of a sequence
|
|
||||||
if not is_last_in_sequence(entry):
|
|
||||||
return False
|
|
||||||
|
|
||||||
entry["dateTime"] = datetime.strptime(
|
|
||||||
str(entry["dateTime"])[:19], "%Y-%m-%d %H:%M:%S"
|
|
||||||
)
|
|
||||||
if entry["dateTime"] < AFTER_DATE:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return entry
|
|
||||||
|
|
||||||
|
|
||||||
def is_last_in_sequence(entry: Dict) -> Boolean:
|
|
||||||
indoor = entry["directionState"]["indoor"]
|
|
||||||
outdoor = entry["directionState"]["outdoor"]
|
|
||||||
|
|
||||||
if len(indoor) <= 0 or len(outdoor) <= 0:
|
|
||||||
return False
|
|
||||||
|
|
||||||
end_key = "end_distance"
|
|
||||||
# Check version
|
|
||||||
if end_key not in indoor[-1]:
|
|
||||||
end_key = "end"
|
|
||||||
|
|
||||||
if indoor[-1][end_key] is None or outdoor[-1][end_key] is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
# Collect
|
|
||||||
log: list[Dict] = [json.loads(line.strip("\x00")) for line in content]
|
|
||||||
print("Number of total entries:", len(log))
|
|
||||||
|
|
||||||
# Parse & Filter
|
|
||||||
log = [parse_log_entry(entry) for entry in log if parse_log_entry(entry)] # type: ignore
|
|
||||||
print("Number of filtered entries:", len(log))
|
|
||||||
|
|
||||||
# Render
|
|
||||||
fig, ax = plt.subplots() # Create a figure containing a single axes.
|
|
||||||
times: list[datetime] = [entry["dateTime"] for entry in log]
|
|
||||||
counts: list[int] = [entry["previousPeopleCount"] for entry in log]
|
|
||||||
ax.step(times, counts, where="pre") # type: ignore
|
|
||||||
print("-"*20)
|
|
||||||
plt.show()
|
|
||||||
print("-" * 20)
|
|
||||||
|
|
||||||
|
|
||||||
# Print stats
|
|
||||||
walk_ins = [entry for entry in log if entry["countChange"] > 0]
|
|
||||||
walk_outs = [entry for entry in log if entry["countChange"] < 0]
|
|
||||||
walk_unders = [entry for entry in log if entry["countChange"] == 0]
|
|
||||||
print("Number of walk-ins:", len(walk_ins))
|
|
||||||
print("Number of walk-outs:", len(walk_outs))
|
|
||||||
print("Number of walk-unders:", len(walk_unders))
|
|
||||||
print("-" * 20)
|
|
||||||
|
|
||||||
# Calculate faults
|
|
||||||
|
|
||||||
for c, n in zip(list(range(len(log))), list(range(len(log)))[1:]):
|
|
||||||
estimated_count: int = log[c]["previousPeopleCount"] + log[c]["countChange"]
|
|
||||||
faulty: bool = estimated_count != log[n]["previousPeopleCount"]
|
|
||||||
log[c]["faulty"] = faulty
|
|
||||||
log[c]["faultyCount"] = log[c]["previousPeopleCount"] if faulty else None
|
|
||||||
|
|
||||||
log = log[:-1]
|
|
||||||
fault_count = sum(1 for entry in log if entry["faulty"])
|
|
||||||
fault_percentage = fault_count / len(log)
|
|
||||||
print("Number of faults:", fault_count)
|
|
||||||
print("Percentage of faults:", fault_percentage * 100, "%")
|
|
||||||
|
|
||||||
print("-" * 20)
|
|
||||||
faulty_off = [entry for entry in log if entry["faulty"] and entry["faultyCount"] == 0]
|
|
||||||
faulty_on = [entry for entry in log if entry["faulty"] and entry["faultyCount"] != 0]
|
|
||||||
print("Number of false-0:", len(faulty_off))
|
|
||||||
print("Number of false-1:", len(faulty_on))
|
|
||||||
print("Percentage of false-0:", len(faulty_off) / fault_count * 100, "%")
|
|
||||||
print("Percentage of false-1:", len(faulty_on) / fault_count * 100, "%")
|
|
||||||
|
|
||||||
# Number of dates
|
|
||||||
unique_dates = set()
|
|
||||||
for entry in log:
|
|
||||||
date = entry["dateTime"].strftime("%Y-%m-%d")
|
|
||||||
unique_dates.add(date)
|
|
||||||
total_days = len(unique_dates)
|
|
||||||
|
|
||||||
print("-"*20)
|
|
||||||
print("Number of days:", total_days)
|
|
||||||
print("Changes per day:", len(log) / total_days)
|
|
||||||
|
|
||||||
corrections_per_day = len(log) / total_days * fault_percentage
|
|
||||||
print("Corrections per day:", corrections_per_day)
|
|
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2018 sankalpjonn
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
|
@ -1 +0,0 @@
|
||||||
from timeloop.app import Timeloop
|
|
|
@ -1,72 +0,0 @@
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
import signal
|
|
||||||
import time
|
|
||||||
|
|
||||||
from timeloop.exceptions import ServiceExit
|
|
||||||
from timeloop.job import Job
|
|
||||||
from timeloop.helpers import service_shutdown
|
|
||||||
|
|
||||||
|
|
||||||
class Timeloop():
|
|
||||||
def __init__(self):
|
|
||||||
self.jobs = []
|
|
||||||
logger = logging.getLogger('timeloop')
|
|
||||||
ch = logging.StreamHandler(sys.stdout)
|
|
||||||
ch.setLevel(logging.INFO)
|
|
||||||
formatter = logging.Formatter('[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s')
|
|
||||||
ch.setFormatter(formatter)
|
|
||||||
logger.addHandler(ch)
|
|
||||||
logger.setLevel(logging.INFO)
|
|
||||||
self.logger = logger
|
|
||||||
|
|
||||||
def _add_job(self, func, interval: timedelta, offset: timedelta=None, *args, **kwargs):
|
|
||||||
j = Job(interval, func, offset=offset, *args, **kwargs)
|
|
||||||
self.jobs.append(j)
|
|
||||||
|
|
||||||
def _block_main_thread(self):
|
|
||||||
signal.signal(signal.SIGTERM, service_shutdown)
|
|
||||||
signal.signal(signal.SIGINT, service_shutdown)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
time.sleep(1)
|
|
||||||
except ServiceExit:
|
|
||||||
self.stop()
|
|
||||||
break
|
|
||||||
|
|
||||||
def _start_jobs(self, block):
|
|
||||||
for j in self.jobs:
|
|
||||||
j.daemon = not block
|
|
||||||
j.start()
|
|
||||||
self.logger.info("Registered job {}".format(j.execute))
|
|
||||||
|
|
||||||
def _stop_jobs(self):
|
|
||||||
for j in self.jobs:
|
|
||||||
self.logger.info("Stopping job {}".format(j.execute))
|
|
||||||
j.stop()
|
|
||||||
|
|
||||||
def job(self, interval: timedelta, offset: timedelta=None):
|
|
||||||
"""Decorator to define a timeloop for the decorated function.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
interval (timedelta): How long to wait after every execution until the next one.
|
|
||||||
offset (timedelta, optional): Positive offset until the first execution of the function. If None, will wait with first execution until the first interval passed. If timedelta with length 0 (or smaller) will execute immediately. Defaults to None.
|
|
||||||
"""
|
|
||||||
def decorator(f):
|
|
||||||
self._add_job(f, interval, offset=offset)
|
|
||||||
return f
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self._stop_jobs()
|
|
||||||
self.logger.info("Timeloop exited.")
|
|
||||||
|
|
||||||
def start(self, block=False):
|
|
||||||
self.logger.info("Starting Timeloop..")
|
|
||||||
self._start_jobs(block=block)
|
|
||||||
|
|
||||||
self.logger.info("Timeloop now started. Jobs will run based on the interval set")
|
|
||||||
if block:
|
|
||||||
self._block_main_thread()
|
|
|
@ -1,6 +0,0 @@
|
||||||
class ServiceExit(Exception):
|
|
||||||
"""
|
|
||||||
Custom exception which is used to trigger the clean exit
|
|
||||||
of all running threads and the main program.
|
|
||||||
"""
|
|
||||||
pass
|
|
|
@ -1,5 +0,0 @@
|
||||||
from timeloop.exceptions import ServiceExit
|
|
||||||
|
|
||||||
|
|
||||||
def service_shutdown(signum, frame):
|
|
||||||
raise ServiceExit
|
|
|
@ -1,25 +0,0 @@
|
||||||
from threading import Thread, Event
|
|
||||||
from datetime import timedelta
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
class Job(Thread):
|
|
||||||
def __init__(self, interval: timedelta, execute, offset: timedelta=None, *args, **kwargs):
|
|
||||||
Thread.__init__(self)
|
|
||||||
self.stopped = Event()
|
|
||||||
self.interval: timedelta = interval
|
|
||||||
self.execute = execute
|
|
||||||
self.offset: timedelta = offset
|
|
||||||
self.args = args
|
|
||||||
self.kwargs = kwargs
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self.stopped.set()
|
|
||||||
self.join()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
if self.offset:
|
|
||||||
sleep(self.offset.total_seconds())
|
|
||||||
self.execute(*self.args, **self.kwargs)
|
|
||||||
|
|
||||||
while not self.stopped.wait(self.interval.total_seconds()):
|
|
||||||
self.execute(*self.args, **self.kwargs)
|
|
Loading…
Reference in a new issue