Skip to content

Commit

Permalink
Add Smart Scene
Browse files Browse the repository at this point in the history
  • Loading branch information
hendriksen-mark committed Aug 16, 2024
1 parent 7100277 commit 5b6202c
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 12 deletions.
2 changes: 1 addition & 1 deletion BridgeEmulator/HueObjects/Group.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
103 changes: 103 additions & 0 deletions BridgeEmulator/HueObjects/SmartScene.py
Original file line number Diff line number Diff line change
@@ -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
13 changes: 9 additions & 4 deletions BridgeEmulator/configManager/configHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"):
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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:
Expand Down
49 changes: 44 additions & 5 deletions BridgeEmulator/flaskUI/v2restapi.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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": {}}

Expand All @@ -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)
Expand Down Expand Up @@ -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"
}

Expand Down Expand Up @@ -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())
Expand All @@ -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():
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()]}
Expand Down Expand Up @@ -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"]:
Expand Down
1 change: 1 addition & 0 deletions BridgeEmulator/functions/daylightSensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 53 additions & 2 deletions BridgeEmulator/services/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 5b6202c

Please sign in to comment.