diff --git a/BridgeEmulator/HueObjects/Group.py b/BridgeEmulator/HueObjects/Group.py index 72da9f23b..67f56461e 100644 --- a/BridgeEmulator/HueObjects/Group.py +++ b/BridgeEmulator/HueObjects/Group.py @@ -188,7 +188,7 @@ def getV1Api(self): sensors.append(sensor().id_v1) result["lights"] = lights result["sensors"] = sensors - result["type"] = self.type + result["type"] = self.type.capitalize() result["state"] = self.update_state() result["recycle"] = False if self.id_v1 == "0": diff --git a/BridgeEmulator/HueObjects/SmartScene.py b/BridgeEmulator/HueObjects/SmartScene.py new file mode 100644 index 000000000..2d103dd41 --- /dev/null +++ b/BridgeEmulator/HueObjects/SmartScene.py @@ -0,0 +1,103 @@ +import uuid +import logManager +from threading import Thread +from datetime import datetime, timezone +from HueObjects import genV2Uuid, StreamEvent + +logging = logManager.logger.get_logger(__name__) + +class SmartScene(): + + DEFAULT_SPEED = 60000#ms + + def __init__(self, data): + self.name = data["name"] + self.id_v1 = data["id_v1"] + self.id_v2 = data["id_v2"] if "id_v2" in data else genV2Uuid() + self.appdata = data["appdata"] if "appdata" in data else {} + self.type = data["type"] if "type" in data else "smart_scene" + self.image = data["image"] if "image" in data else None + self.action = data["action"] if "action" in data else "deactivate" + self.lastupdated = data["lastupdated"] if "lastupdated" in data else datetime.now(timezone.utc + ).strftime("%Y-%m-%dT%H:%M:%S") + self.timeslots = data["timeslots"] if "timeslots" in data else {} + self.recurrence = data["recurrence"] if "recurrence" in data else {} + self.speed = data["transition_duration"] if "transition_duration" in data else self.DEFAULT_SPEED + self.group = data["group"] if "group" in data else None + self.state = data["state"] if "state" in data else "inactive" + self.active_timeslot = data["active_timeslot"] if "active_timeslot" in data else 0 + streamMessage = {"creationtime": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "data": [self.getV2Api()], + "id": str(uuid.uuid4()), + "type": "add" + } + streamMessage["data"][0].update(self.getV2Api()) + StreamEvent(streamMessage) + + def __del__(self): + streamMessage = {"creationtime": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "data": [{"id": self.id_v2, "type": "smart_scene"}], + "id": str(uuid.uuid4()), + "type": "delete" + } + streamMessage["id_v1"] = "/smart_scene/" + self.id_v1 + StreamEvent(streamMessage) + logging.info(self.name + " smart_scene was destroyed.") + + def activate(self, data): + # activate smart scene + if "recall" in data: + if data["recall"]["action"] == "activate": + logging.debug("activate smart_scene: " + self.name + " scene: " + str(self.active_timeslot)) + self.state = "active" + if datetime.now().strftime("%A").lower() in self.recurrence: + from flaskUI.v2restapi import getObject + target_object = getObject(self.timeslots[self.active_timeslot]["target"]["rtype"], self.timeslots[self.active_timeslot]["target"]["rid"]) + putDict = {"recall": {"action": "active", "duration": self.speed}} + target_object.activate(putDict) + return + if data["recall"]["action"] == "deactivate": + from functions.scripts import findGroup + group = findGroup(self.group["rid"]) + group.setV1Action(state={"on": False}) + logging.debug("deactivate smart_scene: " + self.name) + self.state = "inactive" + return + + + def getV2Api(self): + result = {} + result["metadata"] = {} + if self.image != None: + result["metadata"]["image"] = {"rid": self.image, + "rtype": "public_image"} + result["metadata"]["name"] = self.name + result["id"] = self.id_v2 + result["id_v1"] = "/smart_scene/" + self.id_v1 + result["group"] = self.group + result["type"] = "smart_scene" + result["week_timeslots"] = [{"timeslots": self.timeslots, "recurrence": self.recurrence}] + result["transition_duration"] = self.speed + result["state"] = self.state + result["active_timeslot"] = {"timeslot_id": self.active_timeslot if self.active_timeslot >= 0 else len(self.timeslots)-1 , "weekday": str(datetime.now().strftime("%A")).lower()} + return result + + def update_attr(self, newdata): + self.lastupdated = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S") + for key, value in newdata.items(): + updateAttribute = getattr(self, key) + if isinstance(updateAttribute, dict): + updateAttribute.update(value) + setattr(self, key, updateAttribute) + else: + setattr(self, key, value) + + def save(self): + result = {"id_v2": self.id_v2, "name": self.name, "appdata": self.appdata, "type": self.type, "image": self.image, + "lastupdated": self.lastupdated, "state": self.state, "group": self.group, "active_timeslot": self.active_timeslot } + if self.timeslots != None: + result["timeslots"] = self.timeslots + result["recurrence"] = self.recurrence + result["speed"] = self.speed or self.DEFAULT_SPEED + + return result diff --git a/BridgeEmulator/configManager/configHandler.py b/BridgeEmulator/configManager/configHandler.py index 100a3969d..a74d61784 100644 --- a/BridgeEmulator/configManager/configHandler.py +++ b/BridgeEmulator/configManager/configHandler.py @@ -9,8 +9,8 @@ import yaml import uuid import weakref -from HueObjects import Light, Group, EntertainmentConfiguration, Scene, ApiUser, Rule, ResourceLink, Schedule, Sensor, BehaviorInstance from time import sleep +from HueObjects import Light, Group, EntertainmentConfiguration, Scene, ApiUser, Rule, ResourceLink, Schedule, Sensor, BehaviorInstance, SmartScene try: from time import tzset except ImportError: @@ -41,7 +41,7 @@ def __init__(self): os.makedirs(self.configDir) def load_config(self): - self.yaml_config = {"apiUsers": {}, "lights": {}, "groups": {}, "scenes": {}, "config": {}, "rules": {}, "resourcelinks": {}, "schedules": {}, "sensors": {}, "behavior_instance": {}, "geofence_clients": {}, "temp": {"eventstream": [], "scanResult": {"lastscan": "none"}, "detectedLights": [], "gradientStripLights": {}}} + self.yaml_config = {"apiUsers": {}, "lights": {}, "groups": {}, "scenes": {}, "config": {}, "rules": {}, "resourcelinks": {}, "schedules": {}, "sensors": {}, "behavior_instance": {}, "geofence_clients": {}, "smart_scene": {}, "temp": {"eventstream": [], "scanResult": {"lastscan": "none"}, "detectedLights": [], "gradientStripLights": {}}} try: #load config if os.path.exists(self.configDir + "/config.yaml"): @@ -199,7 +199,12 @@ def load_config(self): for light, lightstate in data["lightstates"].items(): lightObj = self.yaml_config["lights"][light] self.yaml_config["scenes"][scene].lightstates[lightObj] = lightstate - + #smart_scene + if os.path.exists(self.configDir + "/smart_scene.yaml"): + smart_scene = _open_yaml(self.configDir + "/smart_scene.yaml") + for scene, data in smart_scene.items(): + data["id_v1"] = scene + self.yaml_config["smart_scene"][scene] = SmartScene.SmartScene(data) #rules if os.path.exists(self.configDir + "/rules.yaml"): rules = _open_yaml(self.configDir + "/rules.yaml") @@ -262,7 +267,7 @@ def save_config(self, backup=False, resource="all"): return saveResources = [] if resource == "all": - saveResources = ["lights", "groups", "scenes", "rules", "resourcelinks", "schedules", "sensors", "behavior_instance"] + saveResources = ["lights", "groups", "scenes", "rules", "resourcelinks", "schedules", "sensors", "behavior_instance", "smart_scene"] else: saveResources.append(resource) for object in saveResources: diff --git a/BridgeEmulator/flaskUI/v2restapi.py b/BridgeEmulator/flaskUI/v2restapi.py index 10748d92e..08993b648 100644 --- a/BridgeEmulator/flaskUI/v2restapi.py +++ b/BridgeEmulator/flaskUI/v2restapi.py @@ -1,6 +1,6 @@ import configManager import logManager -from HueObjects import Group, EntertainmentConfiguration, Scene, BehaviorInstance, GeofenceClient, StreamEvent +from HueObjects import Group, EntertainmentConfiguration, Scene, BehaviorInstance, GeofenceClient, SmartScene, StreamEvent import uuid import json import weakref @@ -15,12 +15,13 @@ from functions.scripts import behaviorScripts from pprint import pprint from lights.discover import scanForLights +from functions.daylightSensor import daylightSensor logging = logManager.logger.get_logger(__name__) bridgeConfig = configManager.bridgeConfig.yaml_config -v2Resources = {"light": {}, "scene": {}, "grouped_light": {}, "room": {}, "zone": { +v2Resources = {"light": {}, "scene": {}, "smart_scene": {}, "grouped_light": {}, "room": {}, "zone": { }, "entertainment": {}, "entertainment_configuration": {}, "zigbee_connectivity": {}, "zigbee_device_discovery": {}, "device": {}, "device_power": {}, "geofence_client": {}, "motion": {}} @@ -31,8 +32,8 @@ def getObject(element, v2uuid): elif element in v2Resources and v2uuid in v2Resources[element]: logging.debug("Cache Hit for " + element) return v2Resources[element][v2uuid]() - elif element in ["light", "scene", "grouped_light"]: - for v1Element in ["lights", "groups", "scenes"]: + elif element in ["light", "scene", "grouped_light", "smart_scene"]: + for v1Element in ["lights", "groups", "scenes", "smart_scene"]: for key, obj in bridgeConfig[v1Element].items(): if obj.id_v2 == v2uuid: v2Resources[element][v2uuid] = weakref.ref(obj) @@ -169,6 +170,10 @@ def geoLocation(): return { "id": str(uuid.uuid5(uuid.NAMESPACE_URL, bridgeConfig["config"]["bridgeid"] + 'geolocation')), "is_configured": bridgeConfig["sensors"]["1"].config["configured"], + "sun_today": { + "sunset_time": bridgeConfig["sensors"]["1"].config["sunset"] if "lat" in bridgeConfig["sensors"]["1"].protocol_cfg else "", + "day_type": "normal_day" + }, "type": "geolocation" } @@ -239,6 +244,9 @@ def get(self): # scenes for key, scene in bridgeConfig["scenes"].items(): data.append(scene.getV2Api()) + # smart_scene + for key, smartscene in bridgeConfig["smart_scene"].items(): + data.append(smartscene.getV2Api()) # lights for key, light in bridgeConfig["lights"].items(): data.append(light.getV2Api()) @@ -261,6 +269,7 @@ def get(self): # bridge home data.append(v2BridgeHome()) data.append(v2GeofenceClient()) + data.append(geoLocation()) for script in behaviorScripts(): data.append(script) for key, sensor in bridgeConfig["sensors"].items(): @@ -295,6 +304,9 @@ def get(self, resource): if resource == "scene": for key, scene in bridgeConfig["scenes"].items(): response["data"].append(scene.getV2Api()) + elif resource == "smart_scene": + for key, smartscene in bridgeConfig["smart_scene"].items(): + response["data"].append(smartscene.getV2Api()) elif resource == "light": for key, light in bridgeConfig["lights"].items(): response["data"].append(light.getV2Api()) @@ -434,6 +446,20 @@ def post(self, resource): if "gradient" in scene: sceneState["gradient"] = scene["gradient"] newObject.lightstates[lightObj] = sceneState + elif resource == "smart_scene": + new_object_id = nextFreeId(bridgeConfig, "smart_scene") + objCreation = { + "id_v1": new_object_id, + "name": postDict["metadata"]["name"], + "image": postDict["metadata"]["image"]["rid"] if "image" in postDict["metadata"] else None, + "action": postDict["recall"]["action"], + "timeslots": postDict["week_timeslots"][0]["timeslots"], + "recurrence": postDict["week_timeslots"][0]["recurrence"] + } + del postDict["week_timeslots"] + objCreation.update(postDict) + newObject = SmartScene.SmartScene(objCreation) + bridgeConfig["smart_scene"][new_object_id] = newObject elif resource == "behavior_instance": newObject = BehaviorInstance.BehaviorInstance(postDict) bridgeConfig["behavior_instance"][newObject.id_v2] = newObject @@ -509,7 +535,7 @@ def get(self, resource, resourceid): if not object: return {"errors": [], "data": []} - if resource in ["scene", "light"]: + if resource in ["scene", "light", "smart_scene"]: return {"errors": [], "data": [object.getV2Api()]} elif resource == "room": return {"errors": [], "data": [object.getV2Room()]} @@ -565,12 +591,25 @@ def put(self, resource, resourceid): object.palette = putDict["palette"] if "metadata" in putDict: object.name = putDict["metadata"]["name"] + elif resource == "smart_scene": + if "recall" in putDict and "action" in putDict["recall"]: + object.activate(putDict) + if "transition_duration" in putDict: + object.speed = putDict["transition_duration"] + if "week_timeslots" in putDict: + if "timeslots" in putDict["week_timeslots"][0]: + object.timeslots = putDict["week_timeslots"][0]["timeslots"] + if "recurrence" in putDict["week_timeslots"][0]: + object.recurrence = putDict["week_timeslots"][0]["recurrence"] + if "metadata" in putDict: + object.name = putDict["metadata"]["name"] elif resource == "grouped_light": object.setV2Action(putDict) elif resource == "geolocation": bridgeConfig["sensors"]["1"].protocol_cfg = { "lat": putDict["latitude"], "long": putDict["longitude"]} bridgeConfig["sensors"]["1"].config["configured"] = True + daylightSensor(bridgeConfig["config"]["timezone"], bridgeConfig["sensors"]["1"]) elif resource == "behavior_instance": object.update_attr(putDict) elif resource in ["room", "zone"]: diff --git a/BridgeEmulator/functions/daylightSensor.py b/BridgeEmulator/functions/daylightSensor.py index d4dd8ba10..de72c5d25 100644 --- a/BridgeEmulator/functions/daylightSensor.py +++ b/BridgeEmulator/functions/daylightSensor.py @@ -26,6 +26,7 @@ def daylightSensor(tz, sensor):#tz = timezone deltaSunriseOffset = deltaSunrise.total_seconds() + sensor.config["sunriseoffset"] * 60 logging.info("deltaSunsetOffset: " + str(deltaSunsetOffset)) logging.info("deltaSunriseOffset: " + str(deltaSunriseOffset)) + sensor.config["sunset"] = s['sunset'].astimezone().strftime("%H:%M:%S") current_time = datetime.now(timezone.utc).replace(tzinfo=None) if deltaSunriseOffset < 0 and deltaSunsetOffset > 0: sensor.state["daylight"] = True diff --git a/BridgeEmulator/services/scheduler.py b/BridgeEmulator/services/scheduler.py index a03493bcd..e9fd7ac16 100644 --- a/BridgeEmulator/services/scheduler.py +++ b/BridgeEmulator/services/scheduler.py @@ -7,8 +7,10 @@ from datetime import datetime, timedelta, time, date, timezone from functions.request import sendRequest from functions.daylightSensor import daylightSensor -from functions.scripts import triggerScript +from functions.scripts import findGroup, triggerScript from services import updateManager +from flaskUI.v2restapi import getObject +from copy import deepcopy bridgeConfig = configManager.bridgeConfig.yaml_config logging = logManager.logger.get_logger(__name__) @@ -100,7 +102,56 @@ def runScheduler(): except Exception as e: - logging.info("Exception while processing the schedule " + obj.name + " | " + str(e)) + logging.info("Exception while processing the behavior_instance " + obj.name + " | " + str(e)) + + for smartscene, obj in bridgeConfig["smart_scene"].items(): + try: + if hasattr(obj, "timeslots"): + sunset_slot = -1 + sunset = bridgeConfig["sensors"]["1"].config["sunset"] if "lat" in bridgeConfig["sensors"]["1"].protocol_cfg else "21:00:00" + slots = deepcopy(obj.timeslots) + if hasattr(obj, "recurrence"): + if datetime.now().strftime("%A").lower() not in obj.recurrence: + continue + for instance, slot in enumerate(slots): + time_object = "" + if slot["start_time"]["kind"] == "time": + time_object = datetime( + year=1, + month=1, + day=1, + hour=slot["start_time"]["time"]["hour"], + minute=slot["start_time"]["time"]["minute"], + second=slot["start_time"]["time"]["second"]).strftime("%H:%M:%S") + elif slot["start_time"]["kind"] == "sunset": + sunset_slot = instance + time_object = sunset + if sunset_slot > 0 and instance == sunset_slot+1: + if sunset > time_object: + time_object = (datetime.strptime(sunset, "%H:%M:%S") + timedelta(minutes=30)).strftime("%H:%M:%S") + slots[instance]["start_time"]["time"] = time_object + slots[instance]["start_time"]["instance"] = instance + + slots = sorted(slots, key=lambda x: datetime.strptime(x["start_time"]["time"], "%H:%M:%S")) + active_timeslot = obj.active_timeslot + for slot in slots: + if datetime.now().strftime("%H:%M:%S") >= slot["start_time"]["time"]: + active_timeslot = slot["start_time"]["instance"] + if obj.active_timeslot != active_timeslot: + obj.active_timeslot = active_timeslot + if obj.state == "active": + if active_timeslot == len(obj.timeslots)-1: + logging.info("stop smart_scene: " + obj.name) + group = findGroup(obj.group["rid"]) + group.setV1Action(state={"on": False}) + else: + logging.info("execute smart_scene: " + obj.name + " scene: " + str(obj.active_timeslot)) + putDict = {"recall": {"action": "active", "duration": obj.speed}} + target_object = getObject(obj.timeslots[active_timeslot]["target"]["rtype"], obj.timeslots[active_timeslot]["target"]["rid"]) + target_object.activate(putDict) + + except Exception as e: + logging.info("Exception while processing the smart_scene " + obj.name + " | " + str(e)) if ("updatetime" not in bridgeConfig["config"]["swupdate2"]["autoinstall"]): bridgeConfig["config"]["swupdate2"]["autoinstall"]["updatetime"] = "T14:00:00"