Hmmmm, more or less working hue stuff
This commit is contained in:
parent
5e8ad4f400
commit
1d46b570f6
9 changed files with 277 additions and 105 deletions
|
@ -19,11 +19,22 @@ class BedscaleWeightResult:
|
|||
|
||||
class BedscaleEntity(Entity):
|
||||
|
||||
def __init__(self, *, ip_address: str, id: str, name: str, room: str):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
ip_address: str,
|
||||
id: str,
|
||||
name: str,
|
||||
room: str,
|
||||
history_length: int = 60 * 5,
|
||||
):
|
||||
super().__init__(id=id, name=name, room=room, device_type="bedscale")
|
||||
self._ip_address = ip_address
|
||||
self._history: list[BedscaleWeightResult] = []
|
||||
self._history_length = history_length
|
||||
self.latest_weight: BedscaleWeightResult = None
|
||||
|
||||
async def poll_weights(self) -> BedscaleWeightResult:
|
||||
async def update(self):
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
tr_request = loop.run_in_executor(None, self.__poll_scale__, "tr")
|
||||
|
@ -31,7 +42,7 @@ class BedscaleEntity(Entity):
|
|||
br_request = loop.run_in_executor(None, self.__poll_scale__, "br")
|
||||
bl_request = loop.run_in_executor(None, self.__poll_scale__, "bl")
|
||||
|
||||
results = BedscaleWeightResult(
|
||||
new_result = BedscaleWeightResult(
|
||||
tr=await tr_request,
|
||||
tl=await tl_request,
|
||||
br=await br_request,
|
||||
|
@ -42,10 +53,15 @@ class BedscaleEntity(Entity):
|
|||
|
||||
# TODO: Keep track of empty-bed weight
|
||||
|
||||
return results
|
||||
self._history.append(new_result)
|
||||
if len(self._history) > self._history_length:
|
||||
self._history = self._history[-self._history_length :]
|
||||
|
||||
def __poll_scale__(self, leg: str) -> float | None:
|
||||
try:
|
||||
return requests.get(f"{self._ip_address}/sensor/{leg}/").json()["value"]
|
||||
except:
|
||||
return None
|
||||
|
||||
def get_history(self) -> list[BedscaleWeightResult]:
|
||||
return self._history
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import json
|
||||
import logging
|
||||
from .hue_light import HueLight
|
||||
from core import Bridge, Group
|
||||
from phue import Bridge as phueBridge
|
||||
from time import sleep
|
||||
|
@ -49,8 +49,35 @@ class HueBridge(Bridge):
|
|||
return self._hue.get_api()
|
||||
|
||||
def get_all_lights(self) -> Group:
|
||||
light_states = await self.list_api()
|
||||
# TODO
|
||||
rooms = {
|
||||
r["name"]: [int(l) for l in r["lights"]]
|
||||
for r in self._hue.get_api()["groups"].values()
|
||||
if r["type"] == "Room"
|
||||
}
|
||||
|
||||
lights = []
|
||||
for l in self._hue.get_light_objects():
|
||||
if l.colormode != "xy":
|
||||
# TODO: Implement light to handle all color modes
|
||||
continue
|
||||
|
||||
room = "Unknown"
|
||||
for room_name, room_lights in rooms.items():
|
||||
if l.light_id in room_lights:
|
||||
room = room_name
|
||||
break
|
||||
|
||||
lights.append(
|
||||
HueLight(
|
||||
hue_bridge=self,
|
||||
hue_light=l,
|
||||
id=f"hue-light-{l.light_id}",
|
||||
name=l.name,
|
||||
room=room,
|
||||
)
|
||||
)
|
||||
|
||||
return Group(entities=lights, id="all-hue-lights", name="All Hue Lights")
|
||||
|
||||
def list_scenes(self) -> dict:
|
||||
return self._hue.get_scene()
|
||||
|
@ -62,7 +89,7 @@ class HueBridge(Bridge):
|
|||
return scene
|
||||
return None
|
||||
|
||||
def set_light(self, lights, command):
|
||||
def set_light(self, lights: int | list[int], command):
|
||||
return self._hue.set_light(lights, command)
|
||||
|
||||
def get_light(self, id, command=None):
|
||||
|
|
|
@ -1,56 +1,129 @@
|
|||
import asyncio
|
||||
from core import Entity, Color
|
||||
from bridges.hue import HueBridge
|
||||
from phue import Light
|
||||
|
||||
|
||||
class HueLight(Entity):
|
||||
MAX_HUE_RANGE: int = 65535
|
||||
MAX_SAT_RANGE: int = 254
|
||||
MAX_BRI_RANGE: int = 254
|
||||
TRANSITION_TIME_SCALE: float = 0.1
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
bridge: HueBridge,
|
||||
initial_state: dict,
|
||||
hue_bridge: "HueBridge",
|
||||
hue_light: Light,
|
||||
id: str,
|
||||
name: str,
|
||||
room: str,
|
||||
groups: list[str] = ...
|
||||
groups: list[str] = [],
|
||||
) -> None:
|
||||
super().__init__(id=id, name=name, room=room, groups=groups)
|
||||
self._bridge: HueBridge = bridge
|
||||
super().__init__(
|
||||
id=id, name=name, room=room, groups=groups, device_type="light"
|
||||
)
|
||||
self._bridge = hue_bridge
|
||||
self._hue_light = hue_light
|
||||
|
||||
self._on = False
|
||||
self._color: Color = Color()
|
||||
self._on: bool = False
|
||||
self._transition_duration_sec = 0
|
||||
|
||||
self.__parse_state__(initial_state)
|
||||
asyncio.run(self.update())
|
||||
|
||||
def __parse_state__(self, state: dict):
|
||||
max_int_value = 255
|
||||
self._on = state["state"]["on"]
|
||||
|
||||
h = state["state"]["hue"] / max_int_value
|
||||
s = state["state"]["sat"] / max_int_value
|
||||
v = state["state"]["bri"] / max_int_value
|
||||
|
||||
# TODO: Update color instead of overwriting it, to better keep track of change?
|
||||
self._color = Color(hue=h, saturation=s, brightness=v)
|
||||
|
||||
def update(self):
|
||||
# TODO
|
||||
pass
|
||||
|
||||
def set_brightness(self, brightness: float):
|
||||
"""Does not turn an entity on if brightness > 0 and entity turned off."""
|
||||
raise NotImplementedError("Entity does not support 'set_brightness' operation.")
|
||||
|
||||
def set_hue(self, hue: float):
|
||||
raise NotImplementedError("Entity does not support 'set_hue' operation.")
|
||||
|
||||
def set_saturation(self, saturation: float):
|
||||
raise NotImplementedError("Entity does not support 'set_saturation' operation.")
|
||||
|
||||
def set_color(self, color: Color):
|
||||
raise NotImplementedError("Entity does not support 'set_color' operation.")
|
||||
|
||||
def set_transition_duration(self, seconds: float):
|
||||
raise NotImplementedError(
|
||||
"Entity does not support 'set_transition_duration' operation."
|
||||
async def __commit_to_device__(self):
|
||||
self._hue_light.transitiontime = (
|
||||
self._transition_duration_sec * HueLight.TRANSITION_TIME_SCALE
|
||||
)
|
||||
|
||||
self._hue_light.on = self._on
|
||||
|
||||
self._hue_light.hue = self._color.hue * HueLight.MAX_HUE_RANGE
|
||||
self._hue_light.saturation = self._color.saturation * HueLight.MAX_SAT_RANGE
|
||||
self._hue_light.brightness = self._color.brightness * HueLight.MAX_BRI_RANGE
|
||||
|
||||
async def update(self):
|
||||
def __internal_update__():
|
||||
self._on = self._hue_light.on
|
||||
|
||||
# TODO: Update Color instead of overwriting to better track change
|
||||
self._color = Color(
|
||||
hue=self._hue_light.hue / HueLight.MAX_HUE_RANGE,
|
||||
saturation=self._hue_light.saturation / HueLight.MAX_SAT_RANGE,
|
||||
brightness=self._hue_light.brightness / HueLight.MAX_BRI_RANGE,
|
||||
)
|
||||
|
||||
await asyncio.get_running_loop().run_in_executor(None, __internal_update__)
|
||||
|
||||
@property
|
||||
def color(self) -> Color:
|
||||
return self._color
|
||||
|
||||
@property
|
||||
def on(self) -> bool:
|
||||
return self._on
|
||||
|
||||
@property
|
||||
def transition_time(self) -> float:
|
||||
return self._transition_duration_sec
|
||||
|
||||
async def set_brightness(self, brightness: float):
|
||||
if self._color.brightness == brightness:
|
||||
return
|
||||
|
||||
self._color.brightness = brightness
|
||||
|
||||
if self._on:
|
||||
await self.__commit_to_device__()
|
||||
|
||||
async def set_hue(self, hue: float):
|
||||
if self._color.hue == hue:
|
||||
return
|
||||
|
||||
self._color.hue = hue
|
||||
|
||||
if self._on:
|
||||
await self.__commit_to_device__()
|
||||
|
||||
async def set_saturation(self, saturation: float):
|
||||
if self._color.saturation == saturation:
|
||||
return
|
||||
|
||||
self._color.saturation = saturation
|
||||
|
||||
if self._on:
|
||||
await self.__commit_to_device__()
|
||||
|
||||
async def set_color(self, color: Color):
|
||||
if self._color == color:
|
||||
return
|
||||
|
||||
# TODO: Avoid overwriting to better track change
|
||||
self._color = color
|
||||
|
||||
if self._on:
|
||||
await self.__commit_to_device__()
|
||||
|
||||
async def set_transition_duration(self, seconds: float):
|
||||
if seconds < 0:
|
||||
raise ValueError(
|
||||
f"Given transition duration in seconds must be greater 0. Instead [{str(seconds)}] was provided."
|
||||
)
|
||||
|
||||
self._transition_duration_sec = seconds
|
||||
|
||||
async def turn_on(self):
|
||||
if self._on:
|
||||
return
|
||||
|
||||
self._on = True
|
||||
|
||||
await self.__commit_to_device__()
|
||||
|
||||
async def turn_off(self):
|
||||
if not self._on:
|
||||
return
|
||||
|
||||
self._on = False
|
||||
|
||||
await self.__commit_to_device__()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
class Color:
|
||||
def __init__(self, *, hue: float, saturation: float, brightness: float):
|
||||
|
||||
def __init__(self, *, hue: float = 0, saturation: float = 0, brightness: float = 0):
|
||||
self.hue = hue
|
||||
self.saturation = saturation
|
||||
self.brightness = brightness
|
||||
|
|
|
@ -16,12 +16,14 @@ class Entity:
|
|||
room: str | None,
|
||||
device_type: str,
|
||||
groups: list[str] = [],
|
||||
update_period: float = 1,
|
||||
) -> None:
|
||||
self._id = id
|
||||
self._name = name
|
||||
self._room = room
|
||||
self._device_type = device_type.strip().lower()
|
||||
self._groups = set(groups)
|
||||
self._update_period: float = update_period
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
|
@ -44,6 +46,34 @@ class Entity:
|
|||
def groups(self) -> set[str]:
|
||||
return self._groups
|
||||
|
||||
@property
|
||||
def update_period(self) -> float:
|
||||
return self._update_period
|
||||
|
||||
@property
|
||||
def color(self) -> Color:
|
||||
raise EntityOpNotSupportedError("color")
|
||||
|
||||
@property
|
||||
def brightness(self) -> float:
|
||||
return self.color.brightness
|
||||
|
||||
@property
|
||||
def hue(self) -> float:
|
||||
return self.color.hue
|
||||
|
||||
@property
|
||||
def saturation(self) -> float:
|
||||
return self.color.saturation
|
||||
|
||||
@property
|
||||
def on(self) -> bool:
|
||||
raise EntityOpNotSupportedError("on")
|
||||
|
||||
@property
|
||||
def transition_time(self) -> float:
|
||||
raise EntityOpNotSupportedError("transition_time")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.name} [{self.id}, type {self.device_type}, room {self.room}, in {len(self.groups)} groups]"
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from fnmatch import fnmatch
|
||||
from .entity import Entity, EntityOpNotSupportedError
|
||||
|
||||
|
||||
|
@ -8,7 +9,7 @@ class Group(Entity):
|
|||
*,
|
||||
entities: list[Entity] = ...,
|
||||
id: str = "group",
|
||||
name: str = "Empty Group"
|
||||
name: str = "Empty Group",
|
||||
):
|
||||
super().__init__(id=id, name=name, room=None, device_type="group")
|
||||
self._entities: list[Entity] = entities
|
||||
|
@ -27,6 +28,18 @@ class Group(Entity):
|
|||
for method_name in methods_to_create:
|
||||
setattr(self, method_name, self._create_group_method(method_name))
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._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.__get_entities_with_specific_property__("id", id)
|
||||
|
||||
async def __call_method__(self, method_name: str, *args, **kwargs):
|
||||
for entity in self._entities:
|
||||
try:
|
||||
|
@ -41,3 +54,44 @@ class Group(Entity):
|
|||
await self.__call_method__(method_name, *args, **kwargs)
|
||||
|
||||
return group_method
|
||||
|
||||
def __get_entities_with_specific_property__(
|
||||
self,
|
||||
property: str,
|
||||
target_pattern: str,
|
||||
) -> list[Entity]:
|
||||
"""Returns all entities for which the property getter matches the desired pattern.
|
||||
|
||||
Args:
|
||||
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 self._entities
|
||||
if fnmatch(
|
||||
getattr(e, property),
|
||||
target_pattern,
|
||||
)
|
||||
],
|
||||
name=f"{property}={target_pattern}",
|
||||
)
|
||||
|
||||
def with_id(self, id_pattern: str) -> "Group":
|
||||
return self.__get_entities_with_specific_property__("id", id_pattern)
|
||||
|
||||
def with_name(self, name_pattern: str) -> "Group":
|
||||
return self.__get_entities_with_specific_property__("name", name_pattern)
|
||||
|
||||
def in_room(self, room_pattern: str) -> "Group":
|
||||
return self.__get_entities_with_specific_property__("room", room_pattern)
|
||||
|
||||
def of_device_type(self, type_pattern: str) -> "Group":
|
||||
return self.__get_entities_with_specific_property__("device_type", type_pattern)
|
||||
|
||||
def in_groups(self, groups_pattern: str) -> "Group":
|
||||
return self.__get_entities_with_specific_property__("groups", groups_pattern)
|
||||
|
|
|
@ -16,38 +16,18 @@ bedscale = BedscaleEntity(
|
|||
room="Max Zimmer",
|
||||
)
|
||||
|
||||
history = []
|
||||
|
||||
measure_delay_secs = 1
|
||||
history_length_secs = 60 * 10
|
||||
|
||||
|
||||
async def bedscale_service():
|
||||
global history
|
||||
global bedscale
|
||||
history_max_num = int(history_length_secs / measure_delay_secs)
|
||||
while True:
|
||||
r = await bedscale.poll_weights()
|
||||
history.append(r)
|
||||
|
||||
if len(history) > history_max_num:
|
||||
history = history[-history_max_num:]
|
||||
|
||||
await asyncio.sleep(1)
|
||||
r = await bedscale.update()
|
||||
await asyncio.sleep(bedscale.update_period)
|
||||
|
||||
|
||||
@router.get("/latest")
|
||||
async def get_latest():
|
||||
if len(history) == 0:
|
||||
if len(bedscale.get_history()) == 0:
|
||||
return HTMLResponse(status_code=200, content="No data given yet")
|
||||
return history[-1]
|
||||
|
||||
|
||||
# @router.get("/file", tags=["file"])
|
||||
# async def get_file():
|
||||
# with open(file_path, "r", encoding="UTF-8") as fp:
|
||||
# return HTMLResponse("\n".join(fp.readlines()))
|
||||
|
||||
return bedscale.get_history()[-1]
|
||||
|
||||
# @router.get("/history")
|
||||
# async def get_history(count: int = None) -> list[dict]:
|
||||
|
@ -74,9 +54,3 @@ async def get_latest():
|
|||
# return points[-count]
|
||||
# else:
|
||||
# return points
|
||||
|
||||
|
||||
# @router.delete("/delete", tags=["file"])
|
||||
# async def delete_file():
|
||||
# os.remove(file_path)
|
||||
# return "Deleted file"
|
||||
|
|
|
@ -9,34 +9,34 @@ from core import Group
|
|||
|
||||
router = APIRouter(tags=["hue"])
|
||||
hue_bridge = HueBridge(ip_address="192.168.178.85", id="hue-bridge")
|
||||
hue_lights: Group = Group()
|
||||
hue_lights: Group = hue_bridge.get_all_lights()
|
||||
|
||||
poll_delay_sec = 5
|
||||
update_period_seconds = 5
|
||||
|
||||
|
||||
async def hue_service():
|
||||
global hue_lights
|
||||
|
||||
hue_lights = await hue_bridge.get_all_lights()
|
||||
|
||||
while True:
|
||||
try:
|
||||
await hue_lights.update()
|
||||
|
||||
await asyncio.sleep(poll_delay_sec)
|
||||
await asyncio.sleep(update_period_seconds)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@router.get("/scenes", tags=["scene"])
|
||||
@router.get("/scenes")
|
||||
async def get_scenes():
|
||||
return hue_bridge.list_scenes()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/room/{room_name}/scene/{scene_name}",
|
||||
tags=["room", "scene"],
|
||||
)
|
||||
@router.get("/lights")
|
||||
async def get_scenes():
|
||||
return {
|
||||
l.id: {"name": l.name, "room": l.room, "color": str(l.color), "on": l.on}
|
||||
for l in hue_lights
|
||||
}
|
||||
|
||||
|
||||
@router.post("/room/{room_name}/scene/{scene_name}")
|
||||
async def activate_scene(room_name: str, scene_name: str):
|
||||
try:
|
||||
hue_bridge.in_room_activate_scene(room_name, scene_name)
|
||||
|
@ -44,23 +44,17 @@ async def activate_scene(room_name: str, scene_name: str):
|
|||
return HTMLResponse(status_code=400, content=str(e))
|
||||
|
||||
|
||||
@router.post(
|
||||
"/room/{room_name}/off",
|
||||
tags=["room"],
|
||||
)
|
||||
@router.post("/room/{room_name}/off")
|
||||
async def deactivate_room(room_name: str):
|
||||
try:
|
||||
hue_bridge.in_room_deactivate_lights(room_name)
|
||||
await hue_lights.in_room(room_name).turn_off()
|
||||
except Exception as e:
|
||||
return HTMLResponse(status_code=400, content=str(e))
|
||||
|
||||
|
||||
@router.post(
|
||||
"/room/{room_name}/on",
|
||||
tags=["room"],
|
||||
)
|
||||
@router.post("/room/{room_name}/on")
|
||||
async def activate_room(room_name: str):
|
||||
try:
|
||||
hue_bridge.in_room_activate_lights(room_name)
|
||||
await hue_lights.in_room(room_name).turn_on()
|
||||
except Exception as e:
|
||||
return HTMLResponse(status_code=400, content=str(e))
|
||||
|
|
|
@ -13,12 +13,15 @@ background_tasks = []
|
|||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Start background services."""
|
||||
fritz_task = asyncio.create_task(track_network_devices(), name="Fritz!Box Tracker")
|
||||
# fritz_task = asyncio.create_task(track_network_devices(), name="Fritz!Box Tracker")
|
||||
bedscale_task = asyncio.create_task(bedscale_service(), name="Polling bed-scale")
|
||||
hue_task = asyncio.create_task(hue_service(), name="Polling Hue Bridge")
|
||||
# hue_task = asyncio.create_task(hue_service(), name="Polling Hue Bridge")
|
||||
|
||||
# TODO: Fix background task execution. ^ these calls are blocking
|
||||
|
||||
# Store references to the tasks
|
||||
background_tasks.extend([fritz_task, bedscale_task, hue_task])
|
||||
# background_tasks.extend([fritz_task, bedscale_task, hue_task])
|
||||
background_tasks.extend([bedscale_task])
|
||||
|
||||
yield
|
||||
|
||||
|
|
Loading…
Reference in a new issue