Skip to content

Commit

Permalink
Partial work to support Hue V2 Sunset (but there's no sunrise exposed…
Browse files Browse the repository at this point in the history
…)...
  • Loading branch information
zim514 committed Dec 17, 2023
1 parent afc7293 commit 0cc2da9
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 57 deletions.
124 changes: 76 additions & 48 deletions script.service.hue/resources/lib/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
# This file is part of script.service.hue
# SPDX-License-Identifier: MIT
# See LICENSE.TXT for more information.

import datetime
import json
import sys
import threading

import requests
import xbmc

from . import ADDON, SETTINGS_CHANGED, ADDONID, AMBI_RUNNING
Expand Down Expand Up @@ -41,15 +41,17 @@ def _commands(monitor, command):
ADDON.openSettings()

elif command == "createHueScene":
hue_connection = hueconnection.HueConnection(monitor, silent=True, discover=False) # don't rediscover, proceed silently
hue_connection = hueconnection.HueConnection(monitor, silent=True,
discover=False) # don't rediscover, proceed silently
if hue_connection.connected:
hue_connection.create_hue_scene()
else:
xbmc.log("[script.service.hue] No bridge found. createHueScene cancelled.")
notification(_("Hue Service"), _("Check Hue Bridge configuration"))

elif command == "deleteHueScene":
hue_connection = hueconnection.HueConnection(monitor, silent=True, discover=False) # don't rediscover, proceed silently
hue_connection = hueconnection.HueConnection(monitor, silent=True,
discover=False) # don't rediscover, proceed silently
if hue_connection.connected:
hue_connection.delete_hue_scene()
else:
Expand All @@ -61,7 +63,8 @@ def _commands(monitor, command):
action = sys.argv[3]
# xbmc.log(f"[script.service.hue] sceneSelect: light_group: {light_group}, action: {action}")

hue_connection = hueconnection.HueConnection(monitor, silent=True, discover=False) # don't rediscover, proceed silently
hue_connection = hueconnection.HueConnection(monitor, silent=True,
discover=False) # don't rediscover, proceed silently
if hue_connection.connected:
hue_connection.configure_scene(light_group, action)
else:
Expand All @@ -72,7 +75,8 @@ def _commands(monitor, command):
light_group = sys.argv[2]
# xbmc.log(f"[script.service.hue] ambiLightSelect light_group_id: {light_group}")

hue_connection = hueconnection.HueConnection(monitor, silent=True, discover=False) # don't rediscover, proceed silently # don't rediscover, proceed silently
hue_connection = hueconnection.HueConnection(monitor, silent=True,
discover=False) # don't rediscover, proceed silently # don't rediscover, proceed silently
if hue_connection.connected:
hue_connection.configure_ambilights(light_group)
else:
Expand All @@ -84,37 +88,34 @@ def _commands(monitor, command):


def _service(monitor):
hue_connection = HueConnection(monitor, silent=ADDON.getSettingBool("disableConnectionMessage"), discover=False)
service_enabled = cache_get("service_enabled")

#### V2 Connection
bridge2 = HueAPIv2(monitor, ip=ADDON.getSetting("bridgeIP"), key=ADDON.getSetting("bridgeUser"))


#################
#### V1 Connection - reliable discovery and config
hue_connection = HueConnection(monitor, silent=ADDON.getSettingBool("disableConnectionMessage"), discover=False)

#### V2 Connection - this has no proper bridge discovery, and missing all kinds of error checking
bridge = HueAPIv2(monitor, ip=ADDON.getSetting("bridgeIP"), key=ADDON.getSetting("bridgeUser"))

if hue_connection.connected:
light_groups = [lightgroup.LightGroup(0, hue_connection, lightgroup.VIDEO),
lightgroup.LightGroup(1, hue_connection, lightgroup.AUDIO),
ambigroup.AmbiGroup(3, hue_connection)]
if bridge.connected and hue_connection.connected:
# light groups still expect a V1 bridge object
light_groups = [lightgroup.LightGroup(0, hue_connection, lightgroup.VIDEO), lightgroup.LightGroup(1, hue_connection, lightgroup.AUDIO), ambigroup.AmbiGroup(3, hue_connection)]

timer = 60
daylight = new_daylight = hue_connection.get_daylight()
#start sunset and midnight timers
timers = Timers(monitor, bridge, light_groups)
timers.start()

cache_set("daylight", daylight)
cache_set("service_enabled", True)
# xbmc.log("[script.service.hue] Core service starting. Connected: {}".format(CONNECTED))

while hue_connection.connected and not monitor.abortRequested():
while hue_connection.connected and bridge.connected and not monitor.abortRequested():

# check if service was just re-enabled and if so activate groups
prev_service_enabled = service_enabled
service_enabled = cache_get("service_enabled")
if service_enabled and not prev_service_enabled:
activate(light_groups)

# if service disabled, stop ambilight._ambi_loop thread
# if service was disabled, stop ambilight thread
if not service_enabled:
AMBI_RUNNING.clear()

Expand All @@ -125,35 +126,12 @@ def _service(monitor):

# reload groups if settings changed, but keep player state
if SETTINGS_CHANGED.is_set():
light_groups = [lightgroup.LightGroup(0, hue_connection, lightgroup.VIDEO, initial_state=light_groups[0].state, video_info_tag=light_groups[0].video_info_tag),
lightgroup.LightGroup(1, hue_connection, lightgroup.AUDIO, initial_state=light_groups[1].state, video_info_tag=light_groups[1].video_info_tag),
ambigroup.AmbiGroup(3, hue_connection, initial_state=light_groups[2].state, video_info_tag=light_groups[2].video_info_tag)]
light_groups = [
lightgroup.LightGroup(0, hue_connection, lightgroup.VIDEO, initial_state=light_groups[0].state, video_info_tag=light_groups[0].video_info_tag),
lightgroup.LightGroup(1, hue_connection, lightgroup.AUDIO, initial_state=light_groups[1].state, video_info_tag=light_groups[1].video_info_tag),
ambigroup.AmbiGroup(3, hue_connection, initial_state=light_groups[2].state, video_info_tag=light_groups[2].video_info_tag)]
SETTINGS_CHANGED.clear()

# every minute, check for sunset & connection
if timer > 59:
timer = 0

# fetch daylight status, reconnect to Hue if it fails
try:
new_daylight = hue_connection.get_daylight()
except requests.exceptions.RequestException:
if hue_connection.reconnect(monitor):
new_daylight = hue_connection.get_daylight()
else:
notification(_("Hue Service"), _("Connection lost. Check settings. Shutting down"))
return

# check if sunset took place
if new_daylight != daylight:
xbmc.log(f"[script.service.hue] Daylight change. current: {daylight}, new: {new_daylight}")
daylight = new_daylight

cache_set("daylight", daylight)
if not daylight and service_enabled:
xbmc.log("[script.service.hue] Sunset activate")
activate(light_groups)
timer += 1
monitor.waitForAbort(1)
xbmc.log("[script.service.hue] Process exiting...")
return
Expand Down Expand Up @@ -211,3 +189,53 @@ def activate(light_groups):
xbmc.log(f"[script.service.hue] in activate g: {g}, light_group_id: {g.light_group_id}")
if ADDON.getSettingBool(f"group{g.light_group_id}_enabled"):
g.activate()


class Timers(threading.Thread):
def __init__(self, monitor, bridge, light_groups):
super().__init__()
self.monitor = monitor
self.bridge = bridge
self.light_groups = light_groups

def run(self):
self._schedule()

def _run_midnight(self):
# get new day's sunset time after midnight
self.bridge.update_sunset()
xbmc.log(f"[script.service.hue] in run_midnight(): 1 minute past midnight, new sunset time: {self.bridge.sunset}")

def _run_sunset(self):
# The function you want to run at sunset
activate(self.light_groups)
xbmc.log(f"[script.service.hue] in run_sunset(): Sunset. ")

@staticmethod
def _calculate_initial_delay(target_time):
# Calculate the delay until the target time
now = datetime.datetime.now().time()
target_datetime = datetime.datetime.combine(datetime.date.today(), target_time)
if target_time < now:
target_datetime += datetime.timedelta(days=1)
delay = (target_datetime - datetime.datetime.now()).total_seconds()
return delay

def _schedule(self):
while not self.monitor.abortRequested():
# Calculate delay for sunset and wait
delay_sunset = self._calculate_initial_delay(self.bridge.sunset)
xbmc.log(f"[script.service.hue] in schedule(): Sunset will run in {delay_sunset} seconds")
if self.monitor.waitForAbort(delay_sunset):
# Abort was requested while waiting. We should exit
break
self._run_sunset()

# Calculate delay for midnight and wait
midnight_plus_one = (datetime.datetime.now() + datetime.timedelta(days=1)).replace(hour=0, minute=1, second=0, microsecond=0).time()
delay_midnight = self._calculate_initial_delay(midnight_plus_one)
xbmc.log(f"[script.service.hue] in schedule(): Midnight will run in {delay_midnight} seconds")
if self.monitor.waitForAbort(delay_midnight):
# Abort was requested while waiting. We should exit
break
self._run_midnight()
32 changes: 27 additions & 5 deletions script.service.hue/resources/lib/huev2.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
# This file is part of script.service.hue
# SPDX-License-Identifier: MIT
# See LICENSE.TXT for more information.

import threading

import requests
import urllib3



import simplejson as json
from simplejson import JSONDecodeError
import datetime

import xbmc
import xbmcgui
Expand All @@ -18,6 +23,8 @@

class HueAPIv2(object):
def __init__(self, monitor, ip=None, key=None, discover=False):
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # Old hue bridges use insecure https

self.session = requests.Session()
self.session.verify = False
# session.headers.update({'hue-application-key': hue_application_key})
Expand All @@ -31,6 +38,7 @@ def __init__(self, monitor, ip=None, key=None, discover=False):
self.ip = ip
self.key = key
self.base_url = None
self.sunset = None
self.monitor = monitor

if ip is not None and key is not None:
Expand All @@ -50,6 +58,7 @@ def connect(self):

if self._check_version():
self.connected = True
self.update_sunset()
return True
else:
self.connected = False
Expand Down Expand Up @@ -166,11 +175,14 @@ def discover(self):

def _check_version(self):
try:
software_version = self.get_attribute_value(self.devices, self.bridge_id, ['product_data', 'software_version'])
software_version = self.get_attribute_value(self.devices, self.bridge_id,
['product_data', 'software_version'])
api_split = software_version.split(".")
except KeyError as error:
notification(_("Hue Service"), _("Bridge outdated. Please update your bridge."), icon=xbmcgui.NOTIFICATION_ERROR)
xbmc.log(f"[script.service.hue] in _version_check(): Connected! Bridge too old: {software_version}, error: {error}")
notification(_("Hue Service"), _("Bridge outdated. Please update your bridge."),
icon=xbmcgui.NOTIFICATION_ERROR)
xbmc.log(
f"[script.service.hue] in _version_check(): Connected! Bridge too old: {software_version}, error: {error}")
return False
except Exception as exc:
reporting.process_exception(exc)
Expand All @@ -180,10 +192,18 @@ def _check_version(self):
xbmc.log(f"[script.service.hue] v2 connect() software version: {software_version}")
return True

notification(_("Hue Service"), _("Bridge outdated. Please update your bridge."), icon=xbmcgui.NOTIFICATION_ERROR)
notification(_("Hue Service"), _("Bridge outdated. Please update your bridge."),
icon=xbmcgui.NOTIFICATION_ERROR)
xbmc.log(f"[script.service.hue] v2 connect(): Connected! Bridge API too old: {software_version}")
return False

def update_sunset(self):
geolocation = self.get("geolocation") # TODO: Support cases where geolocation is not configured on bridge.
xbmc.log(f"[script.service.hue] v2 update_sunset(): geolocation: {geolocation}")
sunset_str = self.search_dict(geolocation, "sunset_time")
self.sunset = datetime.datetime.strptime(sunset_str, '%H:%M:%S').time()
xbmc.log(f"[script.service.hue] v2 update_sunset(): sunset: {self.sunset}")

def get(self, resource):
url = f"{self.base_url}/{resource}"

Expand Down Expand Up @@ -285,3 +305,5 @@ def search_dict(d, key):
item = HueAPIv2.search_dict(d, key)
if item is not None:
return item


2 changes: 1 addition & 1 deletion script.service.hue/resources/lib/kodiutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def cache_get(key: str):

try:
data = json.loads(data_str)
# xbmc.log(f"[script.service.hue] Cache Get: {key}, {data}")
#xbmc.log(f"[script.service.hue] Cache Get: {key}, {data}")
return data
except JSONDecodeError:
# Occurs when Cache is empty or unreadable (Eg. Old SimpleCache data still in memory because Kodi hasn't restarted)
Expand Down
2 changes: 1 addition & 1 deletion script.service.hue/resources/lib/lightgroup.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ def playback_type(self):
@staticmethod
def check_active_time():

daylight = cache_get("daylight")
daylight = cache_get("daylight") #TODO: get daylight from HueAPIv2
xbmc.log("[script.service.hue] Schedule: {}, daylightDisable: {}, daylight: {}, startTime: {}, endTime: {}".format(ADDON.getSettingBool("enableSchedule"), ADDON.getSettingBool("daylightDisable"), daylight,
ADDON.getSettingString("startTime"), ADDON.getSettingString("endTime")))

Expand Down
4 changes: 2 additions & 2 deletions script.service.hue/resources/lib/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def build_menu(base_url, addon_handle):

def _get_status():
enabled = cache_get("service_enabled")
daylight = cache_get("daylight")
daylight = cache_get("daylight") #TODO: get daylight from bridge v2
daylight_disable = ADDON.getSettingBool("daylightDisable")
xbmc.log(f"[script.service.hue] _get_status enabled: {enabled} - {type(enabled)}, daylight: {daylight}, daylight_disable: {daylight_disable}")

Expand All @@ -99,7 +99,7 @@ def _get_status():

def _get_status_icon():
enabled = cache_get("service_enabled")
daylight = cache_get("daylight")
daylight = cache_get("daylight") #TODO: get daylight from bridge v2
daylight_disable = ADDON.getSettingBool("daylightDisable")
# xbmc.log("[script.service.hue] Current status: {}".format(daylight_disable))
if daylight and daylight_disable:
Expand Down

0 comments on commit 0cc2da9

Please sign in to comment.