From 62709b66988506abbe6f6ffebfb5b84ac93efe9d Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 22 Apr 2024 14:05:24 +0200 Subject: [PATCH 1/4] Refactoring everything --- .gitignore | 3 +++ example.conf.yaml | 7 +++++ requirements.txt | 8 +++++- src/core/mash.py | 26 +++++++++++++++++++ src/core/module.py | 9 +++++++ .../handlers/hue.py => hue/hue_adapter.py} | 10 +++---- src/{endpoints/hue.py => hue/hue_module.py} | 22 +++++++++++++--- src/hue_bridge_registered.txt | 15 ----------- src/main.py | 12 +++++---- src/matrix_clock/matrix_clock_adapter.py | 13 ++++++++++ src/matrix_clock/matrix_clock_module.py | 6 +++++ 11 files changed, 102 insertions(+), 29 deletions(-) create mode 100644 example.conf.yaml create mode 100644 src/core/mash.py create mode 100644 src/core/module.py rename src/{endpoints/handlers/hue.py => hue/hue_adapter.py} (90%) rename src/{endpoints/hue.py => hue/hue_module.py} (69%) delete mode 100644 src/hue_bridge_registered.txt create mode 100644 src/matrix_clock/matrix_clock_adapter.py create mode 100644 src/matrix_clock/matrix_clock_module.py diff --git a/.gitignore b/.gitignore index ef5bac5..c866a9b 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,6 @@ cython_debug/ #.idea/ .vscode/settings.json .vscode/launch.json + + +hue_bridge_registered.txt \ No newline at end of file diff --git a/example.conf.yaml b/example.conf.yaml new file mode 100644 index 0000000..444105b --- /dev/null +++ b/example.conf.yaml @@ -0,0 +1,7 @@ +modules: + hue: + - ip: "192.168.178.23" + matrixclock: + - name: wfwfo + - ip: "192.168.178.23" + diff --git a/requirements.txt b/requirements.txt index 3fd5bc7..e0195a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,10 @@ phue # API fastapi -uvicorn[standard] \ No newline at end of file +uvicorn[standard] + +# Clients +requests + +# Config file +pyyaml diff --git a/src/core/mash.py b/src/core/mash.py new file mode 100644 index 0000000..b26630e --- /dev/null +++ b/src/core/mash.py @@ -0,0 +1,26 @@ +import yaml +from fastapi import FastAPI + +from core.module import Module + + +class MaSH: + def __init__(self, config_path: str) -> None: + self.server: FastAPI = FastAPI() + self.config: dict = None + + self._load_config_(config_path) + + def _load_config_(self, config_path: str) -> None: + try: + with open(config_path, "r", encoding="UTF-8") as fp: + self.config = yaml.safe_load(fp) + except FileNotFoundError: + raise f"Config file for MaSH server could not be opened at [{config_path}]." + + def add_module(self, module: Module) -> None: + module.add_routes(self.server) + self.server.include_router(module.get_router()) + + def run(self): + self.server.run() diff --git a/src/core/module.py b/src/core/module.py new file mode 100644 index 0000000..3120f67 --- /dev/null +++ b/src/core/module.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI + + +class Module: + def __init__(self, module_id: str) -> None: + self.module_id = module_id + + def add_routes(self, server: FastAPI) -> None: + pass diff --git a/src/endpoints/handlers/hue.py b/src/hue/hue_adapter.py similarity index 90% rename from src/endpoints/handlers/hue.py rename to src/hue/hue_adapter.py index dd85153..0089ae9 100644 --- a/src/endpoints/handlers/hue.py +++ b/src/hue/hue_adapter.py @@ -3,7 +3,7 @@ from phue import Bridge from pathlib import Path -class HueHandler: +class HueAdapter: """Handler for Hue API calls.""" registered_ips_file = "hue_bridge_registered.txt" @@ -35,15 +35,15 @@ class HueHandler: def get_registered_ips(self) -> list: """Get a list of registered bridge IPs.""" - if not Path(HueHandler.registered_ips_file).is_file(): + if not Path(HueAdapter.registered_ips_file).is_file(): return [] - with open(HueHandler.registered_ips_file, "r") as f: - return f.readlines() + with open(HueAdapter.registered_ips_file, "r") as f: + return [ad.strip() for ad in f.readlines()] def register_bridge(self, bridge_ip: str): """Register a bridge IP.""" - with open(HueHandler.registered_ips_file, "a") as f: + with open(HueAdapter.registered_ips_file, "a") as f: f.write(bridge_ip + "\n") def list_scenes(self) -> dict: diff --git a/src/endpoints/hue.py b/src/hue/hue_module.py similarity index 69% rename from src/endpoints/hue.py rename to src/hue/hue_module.py index 74fd6d2..86c63c4 100644 --- a/src/endpoints/hue.py +++ b/src/hue/hue_module.py @@ -1,9 +1,25 @@ +from fastapi import FastAPI, APIRouter + +from hue.hue_adapter import HueAdapter +from ..core.module import Module from fastapi import APIRouter from fastapi.responses import HTMLResponse -from .handlers.hue import HueHandler -router = APIRouter() -hue = HueHandler("192.168.178.85") +router = APIRouter(tags=["hue"]) +hue = HueAdapter("192.168.178.85") + +########## Module ########## + + +class HueModule(Module): + def __init__(self) -> None: + super().__init__("hue") + + def add_routes(self, server: FastAPI) -> None: + server.include_router(router, prefix="/hue") + + +########## Routes ########## @router.get("/scenes", tags=["scene"]) diff --git a/src/hue_bridge_registered.txt b/src/hue_bridge_registered.txt deleted file mode 100644 index a73b77a..0000000 --- a/src/hue_bridge_registered.txt +++ /dev/null @@ -1,15 +0,0 @@ -192.168.178.85 -192.168.178.85 -192.168.178.85 -192.168.178.85 -192.168.178.85 -192.168.178.85 -192.168.178.85 -192.168.178.85 -192.168.178.85 -192.168.178.85 -192.168.178.85 -192.168.178.85 -192.168.178.85 -192.168.178.85 -192.168.178.85 diff --git a/src/main.py b/src/main.py index f88263b..f715e03 100644 --- a/src/main.py +++ b/src/main.py @@ -1,9 +1,11 @@ -from fastapi import FastAPI -from endpoints.hue import router as hue_router +from core.mash import MaSH +from hue.hue_module import HueModule +from matrix_clock.matrix_clock_module import MatrixClockModule -app = FastAPI() +mash: MaSH = MaSH("config.yaml") -app.include_router(hue_router, prefix="/hue", tags=["hue"]) +mash.add_module(HueModule()) +mash.add_module(MatrixClockModule()) if __name__ == "__main__": - app.run() + mash.run() diff --git a/src/matrix_clock/matrix_clock_adapter.py b/src/matrix_clock/matrix_clock_adapter.py new file mode 100644 index 0000000..204e9e1 --- /dev/null +++ b/src/matrix_clock/matrix_clock_adapter.py @@ -0,0 +1,13 @@ +import asyncio +import requests as r + + +class MatrixClockAdapter: + def __init__(self, ip_address: str) -> None: + self.ip_address = ip_address.strip("/") + + if not self.ip_address.startswith("http"): + self.ip_address = f"http://{self.ip_address}" + + async def turn_off(self): + await asyncio.run(r.post(f"{self.ip_address}/off")) diff --git a/src/matrix_clock/matrix_clock_module.py b/src/matrix_clock/matrix_clock_module.py new file mode 100644 index 0000000..526acc8 --- /dev/null +++ b/src/matrix_clock/matrix_clock_module.py @@ -0,0 +1,6 @@ +from core.module import Module + + +class MatrixClockModule(Module): + def __init__(self) -> None: + super().__init__("matrixclock") From e668fc1eaac642fd4ea713997d0b0f850ed5887f Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 22 Apr 2024 19:36:25 +0200 Subject: [PATCH 2/4] More refactoring, and example config-yaml and code snippets --- example.conf.yaml | 32 +++++++++-- src/core/module.py | 9 ---- src/example.py | 53 +++++++++++++++++++ src/hue/{hue_module.py => hue_feature.py} | 6 +-- src/main.py | 4 +- src/mash/feature.py | 9 ++++ src/{core => mash}/mash.py | 8 +-- ...lock_module.py => matrix_clock_feature.py} | 4 +- 8 files changed, 101 insertions(+), 24 deletions(-) delete mode 100644 src/core/module.py create mode 100644 src/example.py rename src/hue/{hue_module.py => hue_feature.py} (92%) create mode 100644 src/mash/feature.py rename src/{core => mash}/mash.py (75%) rename src/matrix_clock/{matrix_clock_module.py => matrix_clock_feature.py} (50%) diff --git a/example.conf.yaml b/example.conf.yaml index 444105b..c5f5413 100644 --- a/example.conf.yaml +++ b/example.conf.yaml @@ -1,7 +1,31 @@ -modules: +features: hue: - - ip: "192.168.178.23" + hue-bridge: + ip: 192.168.178.23 matrixclock: - - name: wfwfo - - ip: "192.168.178.23" + clock: + ip: 192.168.178.23 +home: + latitude: 52.51860 + longitude: 13.37565 + + rooms: + - id : &hw hallway + name: Flur + doors: + - to: ~ + - to: *bath + - to: *kit + - to: *living + - id : &bath bath + name: Badezimmer + - id : &kit kitchen + name: Küche + - id : &living living + name: Wohnzimmer + doors: + - to: *max + - id : &max max + name: Max' Zimmer + diff --git a/src/core/module.py b/src/core/module.py deleted file mode 100644 index 3120f67..0000000 --- a/src/core/module.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi import FastAPI - - -class Module: - def __init__(self, module_id: str) -> None: - self.module_id = module_id - - def add_routes(self, server: FastAPI) -> None: - pass diff --git a/src/example.py b/src/example.py new file mode 100644 index 0000000..3eca4eb --- /dev/null +++ b/src/example.py @@ -0,0 +1,53 @@ +class Device: + def __init__(self, device_id: str, home: "Home") -> None: + self.device_id: str = device_id + self.home: Home = home + + def trigger_change(self): + self.home.trigger_device_change(self.device_id) + + +class Home: + automations: list["Automation"] + + def _get_device_behaviours_(self, device_id: str) -> list["Automation"]: + return [b for a in self.automations] + + def trigger_device_change(self, device_id: str): + pass + + +class Behaviour: + def __init__(self) -> None: + self.action: function = None + self.rule: function = None + self.devices: list[str] = [] + + +class Automation: + def __init__(self) -> None: + self.behaviours: list[Behaviour] = [] + + def device(self, device_id: str) -> dict: + return {"contrast": 3} + + def trigger(self): + def decorator(func): + return func + + return decorator + + +class PeopleCountEngineV1(Automation): + @Automation.trigger( + devices=["matrixclock"], rule=lambda h: h.device("matrixclock").contrast == 6 + ) + def turn_light_on_sometimes(self, home: Home): + home.room("max").lights().on = True + + +from mash.mash import MaSH + +mash = MaSH() + +mash.add_automation(PeopleCountEngineV1()) diff --git a/src/hue/hue_module.py b/src/hue/hue_feature.py similarity index 92% rename from src/hue/hue_module.py rename to src/hue/hue_feature.py index 86c63c4..72be1cf 100644 --- a/src/hue/hue_module.py +++ b/src/hue/hue_feature.py @@ -1,17 +1,17 @@ from fastapi import FastAPI, APIRouter from hue.hue_adapter import HueAdapter -from ..core.module import Module +from ..mash.feature import Feature from fastapi import APIRouter from fastapi.responses import HTMLResponse router = APIRouter(tags=["hue"]) hue = HueAdapter("192.168.178.85") -########## Module ########## +########## Integration ########## -class HueModule(Module): +class HueIntegration(Feature): def __init__(self) -> None: super().__init__("hue") diff --git a/src/main.py b/src/main.py index f715e03..1e34ed9 100644 --- a/src/main.py +++ b/src/main.py @@ -1,6 +1,6 @@ from core.mash import MaSH -from hue.hue_module import HueModule -from matrix_clock.matrix_clock_module import MatrixClockModule +from hue.hue_feature import HueModule +from matrix_clock.matrix_clock_feature import MatrixClockModule mash: MaSH = MaSH("config.yaml") diff --git a/src/mash/feature.py b/src/mash/feature.py new file mode 100644 index 0000000..ef1ed6b --- /dev/null +++ b/src/mash/feature.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI + + +class Feature: + def __init__(self, feature_id: str) -> None: + self.integration_id = feature_id + + def add_routes(self, server: FastAPI) -> None: + pass diff --git a/src/core/mash.py b/src/mash/mash.py similarity index 75% rename from src/core/mash.py rename to src/mash/mash.py index b26630e..64b649d 100644 --- a/src/core/mash.py +++ b/src/mash/mash.py @@ -1,7 +1,7 @@ import yaml from fastapi import FastAPI -from core.module import Module +from mash.feature import Feature class MaSH: @@ -18,9 +18,9 @@ class MaSH: except FileNotFoundError: raise f"Config file for MaSH server could not be opened at [{config_path}]." - def add_module(self, module: Module) -> None: - module.add_routes(self.server) - self.server.include_router(module.get_router()) + def add_integration(self, feature: Feature) -> None: + feature.add_routes(self.server) + self.server.include_router(feature.get_router()) def run(self): self.server.run() diff --git a/src/matrix_clock/matrix_clock_module.py b/src/matrix_clock/matrix_clock_feature.py similarity index 50% rename from src/matrix_clock/matrix_clock_module.py rename to src/matrix_clock/matrix_clock_feature.py index 526acc8..fb03235 100644 --- a/src/matrix_clock/matrix_clock_module.py +++ b/src/matrix_clock/matrix_clock_feature.py @@ -1,6 +1,6 @@ -from core.module import Module +from mash.feature import Feature -class MatrixClockModule(Module): +class MatrixClockIntegration(Feature): def __init__(self) -> None: super().__init__("matrixclock") From df4957d618ad3bfde4d43594988fe2c4af703e02 Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Thu, 2 May 2024 17:21:22 +0200 Subject: [PATCH 3/4] Progress on new server --- README.md | 2 +- example.conf.yaml | 5 +++ src/example.py | 39 ++++++++++++++++++++-- src/mash/entities/entity.py | 32 ++++++++++++++++++ src/mash/entities/group.py | 66 +++++++++++++++++++++++++++++++++++++ src/mash/home.py | 7 ++++ 6 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 src/mash/entities/entity.py create mode 100644 src/mash/entities/group.py create mode 100644 src/mash/home.py diff --git a/README.md b/README.md index 34540e7..fe304df 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Max's Smart Home - MaSH +# Max' Smart Home - MaSH Should be a very simple **server** implementation of what is required in Max's smart home. Trying not to overcomplicate things and thereby ruin motivation to work on this. diff --git a/example.conf.yaml b/example.conf.yaml index c5f5413..4ea3dbb 100644 --- a/example.conf.yaml +++ b/example.conf.yaml @@ -9,6 +9,11 @@ features: home: latitude: 52.51860 longitude: 13.37565 + + beds: + - id: max-bed + name: Bettwaage + room: *max rooms: - id : &hw hallway diff --git a/src/example.py b/src/example.py index 3eca4eb..c4849c2 100644 --- a/src/example.py +++ b/src/example.py @@ -33,19 +33,54 @@ class Automation: def trigger(self): def decorator(func): - return func + return func return decorator class PeopleCountEngineV1(Automation): @Automation.trigger( - devices=["matrixclock"], rule=lambda h: h.device("matrixclock").contrast == 6 + devices=["matrixclock"], + rule=lambda h: h.device("matrixclock").contrast == 6 ) def turn_light_on_sometimes(self, home: Home): home.room("max").lights().on = True + + + + + + + @Automation.trigger( + people=["max"], + rule=lambda h: h.person("max").athome() + ) + def turn_light_on_sometimes(self, home: Home): + home.room("max").lights().on = h.person("max").athome() + + + + + +@Automation.state(h.room("Max").lights()) +def max_room_light(): + if max.ishome(): + return "off" + + scene = "Daylight scene" + + if nighttime: + scene = "nighttime" + + if max.working: + scene.dim(0.5) + + return scene + + + from mash.mash import MaSH mash = MaSH() diff --git a/src/mash/entities/entity.py b/src/mash/entities/entity.py new file mode 100644 index 0000000..317980b --- /dev/null +++ b/src/mash/entities/entity.py @@ -0,0 +1,32 @@ +class Entity: + def __init__( + self, *, id: str, name: str, room: str, device_type: str, groups: list[str] = [] + ) -> None: + self._id = id + self._name = name + self._room = room + self._device_type = device_type + self._groups = set(groups) + + @property + def id(self) -> str: + return self._id + + @property + def name(self) -> str: + return self._name + + @property + def room(self) -> str: + return self._room + + @property + def device_type(self) -> str: + return self._device_type + + @property + def groups(self) -> set[str]: + return self._groups + + def __str__(self) -> str: + return f"{self.name} [{self.id}, type {self.device_type}, room {self.room}, in {len(self.groups)} groups]" diff --git a/src/mash/entities/group.py b/src/mash/entities/group.py new file mode 100644 index 0000000..04b818c --- /dev/null +++ b/src/mash/entities/group.py @@ -0,0 +1,66 @@ +from mash.entities.entity import Entity +from fnmatch import fnmatch + + +class Group: + def __init__(self, *, entities: list[Entity] = []) -> None: + self.entities: list[Entity] = entities + + def __len__(self): + return len(self.entities) + + def __getitem__(self, id: str) -> "Group": + if type(id) is int: + raise "Numerical index not supported." + + return self.id(id) + + def __get_entities_with_specific_property__( + entities: list[Entity], + property_getter: callable[[Entity], str], + target_pattern: str, + ) -> list[Entity]: + """Returns all entities for which the property getter matches the desired pattern. + + Args: + entities (list[Entity]): List of entities. + property_getter (callable[[Entity], str]): Takes one entity and returns the value of the filtered property. + target_pattern (str): Pattern that is matched against. + + Returns: + list[Entity]: A new group of entities or an empty group, if no entity property matches the target pattern. + """ + return Group( + entities=[ + e for e in entities if fnmatch(property_getter(e), target_pattern) + ] + ) + + def device_type(self, device_type: str) -> "Group": + return Group.__get_entities_with_specific_property__( + self.entities, lambda e: e.device_type, device_type + ) + + def id(self, id: str) -> "Group": + return Group.__get_entities_with_specific_property__( + self.entities, lambda e: e.id, id + ) + + def room(self, room: str) -> "Group": + return Group.__get_entities_with_specific_property__( + self.entities, lambda e: e.room, room + ) + + def name(self, name: str) -> "Group": + return Group.__get_entities_with_specific_property__( + self.entities, lambda e: e.name, name + ) + + def lights(self) -> "Group": + return self.device_type("light") + + def beds(self) -> "Group": + return self.device_type("bed") + + def max(self) -> "Group": + return self.room("max") diff --git a/src/mash/home.py b/src/mash/home.py new file mode 100644 index 0000000..a3778b5 --- /dev/null +++ b/src/mash/home.py @@ -0,0 +1,7 @@ +from mash.entities.entity import Entity +from mash.entities.group import Group + + +class Home(Group): + def __init__(self, *, entities: list[Entity] = []) -> None: + super().__init__(entities=entities) From d90463069ad90aaa435553fc6b6369a66498cc80 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 6 May 2024 15:26:24 +0200 Subject: [PATCH 4/4] Renamed src folder back to src --- {src-new => src}/endpoints/bettwaage.py | 0 {src-new => src}/endpoints/handlers/hue.py | 0 {src-new => src}/endpoints/hue.py | 0 {src-new => src}/example.py | 0 {src-new => src}/hue/hue_adapter.py | 0 {src-new => src}/hue/hue_feature.py | 0 {src-new => src}/main.py | 0 {src-new => src}/mash/entities/entity.py | 0 {src-new => src}/mash/entities/group.py | 0 {src-new => src}/mash/feature.py | 0 {src-new => src}/mash/home.py | 0 {src-new => src}/mash/mash.py | 0 {src-new => src}/matrix_clock/matrix_clock_adapter.py | 0 {src-new => src}/matrix_clock/matrix_clock_feature.py | 0 {src-new => src}/old_philips_hue_examples.py | 0 15 files changed, 0 insertions(+), 0 deletions(-) rename {src-new => src}/endpoints/bettwaage.py (100%) rename {src-new => src}/endpoints/handlers/hue.py (100%) rename {src-new => src}/endpoints/hue.py (100%) rename {src-new => src}/example.py (100%) rename {src-new => src}/hue/hue_adapter.py (100%) rename {src-new => src}/hue/hue_feature.py (100%) rename {src-new => src}/main.py (100%) rename {src-new => src}/mash/entities/entity.py (100%) rename {src-new => src}/mash/entities/group.py (100%) rename {src-new => src}/mash/feature.py (100%) rename {src-new => src}/mash/home.py (100%) rename {src-new => src}/mash/mash.py (100%) rename {src-new => src}/matrix_clock/matrix_clock_adapter.py (100%) rename {src-new => src}/matrix_clock/matrix_clock_feature.py (100%) rename {src-new => src}/old_philips_hue_examples.py (100%) diff --git a/src-new/endpoints/bettwaage.py b/src/endpoints/bettwaage.py similarity index 100% rename from src-new/endpoints/bettwaage.py rename to src/endpoints/bettwaage.py diff --git a/src-new/endpoints/handlers/hue.py b/src/endpoints/handlers/hue.py similarity index 100% rename from src-new/endpoints/handlers/hue.py rename to src/endpoints/handlers/hue.py diff --git a/src-new/endpoints/hue.py b/src/endpoints/hue.py similarity index 100% rename from src-new/endpoints/hue.py rename to src/endpoints/hue.py diff --git a/src-new/example.py b/src/example.py similarity index 100% rename from src-new/example.py rename to src/example.py diff --git a/src-new/hue/hue_adapter.py b/src/hue/hue_adapter.py similarity index 100% rename from src-new/hue/hue_adapter.py rename to src/hue/hue_adapter.py diff --git a/src-new/hue/hue_feature.py b/src/hue/hue_feature.py similarity index 100% rename from src-new/hue/hue_feature.py rename to src/hue/hue_feature.py diff --git a/src-new/main.py b/src/main.py similarity index 100% rename from src-new/main.py rename to src/main.py diff --git a/src-new/mash/entities/entity.py b/src/mash/entities/entity.py similarity index 100% rename from src-new/mash/entities/entity.py rename to src/mash/entities/entity.py diff --git a/src-new/mash/entities/group.py b/src/mash/entities/group.py similarity index 100% rename from src-new/mash/entities/group.py rename to src/mash/entities/group.py diff --git a/src-new/mash/feature.py b/src/mash/feature.py similarity index 100% rename from src-new/mash/feature.py rename to src/mash/feature.py diff --git a/src-new/mash/home.py b/src/mash/home.py similarity index 100% rename from src-new/mash/home.py rename to src/mash/home.py diff --git a/src-new/mash/mash.py b/src/mash/mash.py similarity index 100% rename from src-new/mash/mash.py rename to src/mash/mash.py diff --git a/src-new/matrix_clock/matrix_clock_adapter.py b/src/matrix_clock/matrix_clock_adapter.py similarity index 100% rename from src-new/matrix_clock/matrix_clock_adapter.py rename to src/matrix_clock/matrix_clock_adapter.py diff --git a/src-new/matrix_clock/matrix_clock_feature.py b/src/matrix_clock/matrix_clock_feature.py similarity index 100% rename from src-new/matrix_clock/matrix_clock_feature.py rename to src/matrix_clock/matrix_clock_feature.py diff --git a/src-new/old_philips_hue_examples.py b/src/old_philips_hue_examples.py similarity index 100% rename from src-new/old_philips_hue_examples.py rename to src/old_philips_hue_examples.py