From 0cc2da91b3eb06bc0f90b5877f97773b41c7ca34 Mon Sep 17 00:00:00 2001 From: zim514 Date: Sun, 17 Dec 2023 14:51:58 -0500 Subject: [PATCH] Partial work to support Hue V2 Sunset (but there's no sunrise exposed)... --- script.service.hue/resources/lib/core.py | 124 +++++++++++------- script.service.hue/resources/lib/huev2.py | 32 ++++- script.service.hue/resources/lib/kodiutils.py | 2 +- .../resources/lib/lightgroup.py | 2 +- script.service.hue/resources/lib/menu.py | 4 +- 5 files changed, 107 insertions(+), 57 deletions(-) diff --git a/script.service.hue/resources/lib/core.py b/script.service.hue/resources/lib/core.py index 1eec216d..94ff1cea 100644 --- a/script.service.hue/resources/lib/core.py +++ b/script.service.hue/resources/lib/core.py @@ -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 @@ -41,7 +41,8 @@ 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: @@ -49,7 +50,8 @@ def _commands(monitor, command): 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: @@ -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: @@ -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: @@ -84,29 +88,26 @@ 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 @@ -114,7 +115,7 @@ def _service(monitor): 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() @@ -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 @@ -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() diff --git a/script.service.hue/resources/lib/huev2.py b/script.service.hue/resources/lib/huev2.py index 090e646f..68006fa4 100644 --- a/script.service.hue/resources/lib/huev2.py +++ b/script.service.hue/resources/lib/huev2.py @@ -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 @@ -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}) @@ -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: @@ -50,6 +58,7 @@ def connect(self): if self._check_version(): self.connected = True + self.update_sunset() return True else: self.connected = False @@ -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) @@ -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}" @@ -285,3 +305,5 @@ def search_dict(d, key): item = HueAPIv2.search_dict(d, key) if item is not None: return item + + diff --git a/script.service.hue/resources/lib/kodiutils.py b/script.service.hue/resources/lib/kodiutils.py index d571e6fa..f0af3304 100644 --- a/script.service.hue/resources/lib/kodiutils.py +++ b/script.service.hue/resources/lib/kodiutils.py @@ -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) diff --git a/script.service.hue/resources/lib/lightgroup.py b/script.service.hue/resources/lib/lightgroup.py index 31236a86..e07a3e16 100644 --- a/script.service.hue/resources/lib/lightgroup.py +++ b/script.service.hue/resources/lib/lightgroup.py @@ -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"))) diff --git a/script.service.hue/resources/lib/menu.py b/script.service.hue/resources/lib/menu.py index 1e6bd198..8c7cccd5 100644 --- a/script.service.hue/resources/lib/menu.py +++ b/script.service.hue/resources/lib/menu.py @@ -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}") @@ -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: