Merge branch 'master' of https://github.com/mgfcf/MaSH
This commit is contained in:
commit
4c535a58f1
29 changed files with 585 additions and 17 deletions
161
.gitignore
vendored
Normal file
161
.gitignore
vendored
Normal file
|
@ -0,0 +1,161 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
.vscode/settings.json
|
19
mash.yaml
Normal file
19
mash.yaml
Normal file
|
@ -0,0 +1,19 @@
|
|||
# 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,6 +1,12 @@
|
|||
smbus2
|
||||
vl53l1x
|
||||
|
||||
# To parse the config file
|
||||
pyyaml
|
||||
|
||||
# To hot reload the config file
|
||||
watchdog
|
||||
|
||||
# For Philips Hue Counter
|
||||
phue
|
||||
|
||||
|
|
36
src/config/hotdogtrigger.py
Normal file
36
src/config/hotdogtrigger.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
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.")
|
49
src/config/parser.py
Normal file
49
src/config/parser.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
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()
|
26
src/config/reloadtrigger.py
Normal file
26
src/config/reloadtrigger.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
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()
|
48
src/mash.py
Normal file
48
src/mash.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
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
|
23
src/models/colors.py
Normal file
23
src/models/colors.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
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
|
9
src/models/devices/__init__.py
Normal file
9
src/models/devices/__init__.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
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
|
25
src/models/devices/colordevices.py
Normal file
25
src/models/devices/colordevices.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
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
|
5
src/models/devices/genericdevices.py
Normal file
5
src/models/devices/genericdevices.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
class GenericDevice:
|
||||
"""A generic device."""
|
||||
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
43
src/models/devices/switchdevices.py
Normal file
43
src/models/devices/switchdevices.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
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
|
2
src/models/exceptions.py
Normal file
2
src/models/exceptions.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
class NoDeviceFoundError(Exception):
|
||||
pass
|
71
src/models/groups.py
Normal file
71
src/models/groups.py
Normal file
|
@ -0,0 +1,71 @@
|
|||
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
|
28
src/models/helper.py
Normal file
28
src/models/helper.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
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
|
19
src/models/home.py
Normal file
19
src/models/home.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
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,6 +1,6 @@
|
|||
from datetime import datetime, time, timedelta
|
||||
from typing import Dict
|
||||
from services.philips_hue import PhilipsHue
|
||||
from old.philips_hue import PhilipsHue
|
||||
from sensors import PeopleCounter, Directions, VL53L1XSensor
|
||||
import logging
|
||||
import json
|
|
@ -22,7 +22,8 @@ def parse_log_entry(entry: Dict) -> Dict | bool:
|
|||
return False
|
||||
|
||||
entry["dateTime"] = datetime.strptime(
|
||||
str(entry["dateTime"])[:19], "%Y-%m-%d %H:%M:%S")
|
||||
str(entry["dateTime"])[:19], "%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
if entry["dateTime"] < AFTER_DATE:
|
||||
return False
|
||||
|
||||
|
@ -48,18 +49,18 @@ def is_last_in_sequence(entry: Dict) -> Boolean:
|
|||
|
||||
|
||||
# Collect
|
||||
log = [json.loads(line.strip("\x00")) for line in content]
|
||||
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)]
|
||||
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")
|
||||
ax.step(times, counts, where="pre") # type: ignore
|
||||
print("-"*20)
|
||||
plt.show()
|
||||
print("-" * 20)
|
||||
|
@ -77,8 +78,7 @@ 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"]
|
||||
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
|
||||
|
@ -90,10 +90,8 @@ 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]
|
||||
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, "%")
|
Loading…
Reference in a new issue