class PhillipsHueSkill(MycroftSkill): def __init__(self): super(PhillipsHueSkill, self).__init__(name="PhillipsHueSkill") self.brightness_step = int(self.settings.get('brightness_step')) self.color_temperature_step = \ int(self.settings.get('color_temperature_step')) self.verbose = self.settings.get('verbose', False) self.username = self.settings.get('username') if self.username == '': self.username = None self.ip = None # set in _connect_to_bridge self.bridge = None self.default_group = None self.groups_to_ids_map = dict() self.scenes_to_ids_map = dict() @property def connected(self): return self.bridge is not None @property def user_supplied_ip(self): return self.settings.get('ip') != '' @property def user_supplied_username(self): return self.settings.get('username') != '' def _register_with_bridge(self): """ Helper for connecting to the bridge. If we don't have a valid username for the bridge (ip) we are trying to use, this will cause one to be generated. """ self.speak_dialog('connect.to.bridge') i = 0 while i < 30: sleep(1) try: self.bridge = Bridge(self.ip) except PhueRegistrationException: continue else: break if not self.connected: self.speak_dialog('failed.to.register') else: self.speak_dialog('successfully.registered') def _update_bridge_data(self): """ This should be called any time a successful connection is established. It sets some member variables, and ensures that scenes and groups are registered as vocab. """ self.username = self.bridge.username with self.file_system.open('username', 'w') as conf_file: conf_file.write(self.username) if not self.default_group: self._set_default_group(self.settings.get('default_group')) self._register_groups_and_scenes() def _attempt_connection(self): """ This will attempt to connect to the bridge, but will not handle any errors on it's own. Raises ------ UnauthorizedUserException If self.username is not None, and is not registered with the bridge """ if self.user_supplied_ip: self.ip = self.settings.get('ip') else: self.ip = _discover_bridge() if self.username: url = 'http://{ip}/api/{user}'.format(ip=self.ip, user=self.username) data = get(url).json() data = data[0] if isinstance(data, list) else data error = data.get('error') if error: description = error.get('description') if description == "unauthorized user": raise UnauthorizedUserException(self.username) else: raise Exception('Unknown Error: {0}'.format(description)) self.bridge = Bridge(self.ip, self.username) def _connect_to_bridge(self, acknowledge_successful_connection=False): """ Calls _attempt_connection, handling various exceptions by either alerting the user to issues with the config/setup, or registering the application with the bridge. Parameters ---------- acknowledge_successful_connection : bool Speak when a successful connection is established. Returns ------- bool True if a connection is established. """ try: self._attempt_connection() except DeviceNotFoundException: self.speak_dialog('bridge.not.found') return False except ConnectionError: self.speak_dialog('failed.to.connect') if self.user_supplied_ip: self.speak_dialog('ip.in.config') return False except socket.error as e: if 'No route to host' in e.args: self.speak_dialog('no.route') else: self.speak_dialog('failed.to.connect') return False except UnauthorizedUserException: if self.user_supplied_username: self.speak_dialog('invalid.user') return False else: self._register_with_bridge() except PhueRegistrationException: self._register_with_bridge() if not self.connected: return False if acknowledge_successful_connection: self.speak_dialog('successfully.connected') self._update_bridge_data() return True def _set_default_group(self, identifier): """ Sets the group to which commands will be applied, when a group is not specified in the command. Parameters ---------- identifier : str or int The name of the group, or it's integer id Notes ----- If the group does not exist, 0 (all lights) will be used. """ try: self.default_group = Group(self.bridge, identifier) except LookupError: self.speak_dialog('could.not.find.group', {'name': identifier}) self.speak_dialog('using.group.0') self.default_group = Group(self.bridge, 0) def _register_groups_and_scenes(self): """ Register group and scene names as vocab, and update our caches. """ groups = self.bridge.get_group() for id, group in groups.items(): name = group['name'].lower() self.groups_to_ids_map[name] = id self.register_vocabulary(name, "Group") scenes = self.bridge.get_scene() for id, scene in scenes.items(): name = scene['name'].lower() self.scenes_to_ids_map[name] = id self.register_vocabulary(name, "Scene") def initialize(self): """ Attempt to connect to the bridge, and build/register intents. """ self.load_data_files(dirname(__file__)) if self.file_system.exists('username'): if not self.user_supplied_username: with self.file_system.open('username', 'r') as conf_file: self.username = conf_file.read().strip(' \n') try: self._attempt_connection() self._update_bridge_data() except (PhueRegistrationException, DeviceNotFoundException, UnauthorizedUserException, ConnectionError, socket.error): # Swallow it for now; _connect_to_bridge will deal with it pass toggle_intent = IntentBuilder("ToggleIntent") \ .one_of("OffKeyword", "OnKeyword") \ .one_of("Group", "LightsKeyword") \ .build() self.register_intent(toggle_intent, self.handle_toggle_intent) activate_scene_intent = IntentBuilder("ActivateSceneIntent") \ .require("Scene") \ .one_of("Group", "LightsKeyword") \ .build() self.register_intent(activate_scene_intent, self.handle_activate_scene_intent) adjust_brightness_intent = IntentBuilder("AdjustBrightnessIntent") \ .one_of("IncreaseKeyword", "DecreaseKeyword", "DimKeyword") \ .one_of("Group", "LightsKeyword") \ .optionally("BrightnessKeyword") \ .build() self.register_intent(adjust_brightness_intent, self.handle_adjust_brightness_intent) set_brightness_intent = IntentBuilder("SetBrightnessIntent") \ .require("Value") \ .one_of("Group", "LightsKeyword") \ .optionally("BrightnessKeyword") \ .build() self.register_intent(set_brightness_intent, self.handle_set_brightness_intent) adjust_color_temperature_intent = \ IntentBuilder("AdjustColorTemperatureIntent") \ .one_of("IncreaseKeyword", "DecreaseKeyword") \ .one_of("Group", "LightsKeyword") \ .require("ColorTemperatureKeyword") \ .build() self.register_intent(adjust_color_temperature_intent, self.handle_adjust_color_temperature_intent) connect_lights_intent = \ IntentBuilder("ConnectLightsIntent") \ .require("ConnectKeyword") \ .one_of("Group", "LightsKeyword") \ .build() self.register_intent(connect_lights_intent, self.handle_connect_lights_intent) @intent_handler def handle_toggle_intent(self, message, group): if "OffKeyword" in message.data: dialog = 'turn.off' group.on = False else: dialog = 'turn.on' group.on = True if self.verbose: self.speak_dialog(dialog) @intent_handler def handle_activate_scene_intent(self, message, group): scene_name = message.data['Scene'].lower() scene_id = self.scenes_to_ids_map[scene_name] if scene_id: if self.verbose: self.speak_dialog('activate.scene', {'scene': scene_name}) self.bridge.activate_scene(group.group_id, scene_id) else: self.speak_dialog('scene.not.found', {'scene': scene_name}) @intent_handler def handle_adjust_brightness_intent(self, message, group): if "IncreaseKeyword" in message.data: brightness = group.brightness + self.brightness_step group.brightness = \ brightness if brightness < 255 else 254 dialog = 'increase.brightness' else: brightness = group.brightness - self.brightness_step group.brightness = brightness if brightness > 0 else 0 dialog = 'decrease.brightness' if self.verbose: self.speak_dialog(dialog) @intent_handler def handle_set_brightness_intent(self, message, group): value = int(message.data['Value'].rstrip('%')) brightness = int(value / 100.0 * 254) group.on = True group.brightness = brightness if self.verbose: self.speak_dialog('set.brightness', {'brightness': value}) @intent_handler def handle_adjust_color_temperature_intent(self, message, group): if "IncreaseKeyword" in message.data: color_temperature = \ group.colortemp_k + self.color_temperature_step group.colortemp_k = \ color_temperature if color_temperature < 6500 else 6500 dialog = 'increase.color.temperature' else: color_temperature = \ group.colortemp_k - self.color_temperature_step group.colortemp_k = \ color_temperature if color_temperature > 2000 else 2000 dialog = 'decrease.color.temperature' if self.verbose: self.speak_dialog(dialog) @intent_handler def handle_connect_lights_intent(self, message, group): if self.user_supplied_ip: self.speak_dialog('ip.in.config') return if self.verbose: self.speak_dialog('connecting') self._connect_to_bridge(acknowledge_successful_connection=True) def stop(self): pass
class Component(ThreadComponent): MATRIX = matrices.LIGHT_BULB def __init__(self, component_config): super().__init__(component_config) self.bridge = Bridge(component_config['ip_address'], component_config['username']) self.delta_range = range(-254, 254) self.delta = 0 # Extract light IDs, they are stored with format `<bridgeID>-light-<lightID>` light_ids = component_config['device_ids'] light_ids = [i.split('-light-')[1].strip() for i in light_ids] self.lights = self.create_lights(light_ids) self.lights.update_state() self.station_id_1 = component_config.get('station1', None) self.station_id_2 = component_config.get('station2', None) self.station_id_3 = component_config.get('station3', None) # seed random nr generator (used to get random color value) seed() def create_lights(self, light_ids): reachable_lights = self.filter_reachable(light_ids) if not reachable_lights: lights = EmptyLightSet() elif len(reachable_lights) > 10: lights = Group(self.bridge, reachable_lights) else: lights = LightSet(self.bridge, reachable_lights) return lights def filter_reachable(self, light_ids): lights = self.bridge.get_light() reachable = [ i for i in light_ids if i in lights and lights[i]['state']['reachable'] ] logger.debug("lights: %s reachable: %s", list(lights.keys()), reachable) return reachable def on_button_press(self): self.set_light_attributes(on=not self.lights.on) def on_longtouch_left(self): logger.debug("on_longtouch_left()") if self.station_id_1 is not None: self.bridge.activate_scene('0', self.station_id_1) self.nuimo.display_matrix(matrices.STATION1) def on_longtouch_bottom(self): logger.debug("on_longtouch_bottom()") if self.station_id_2 is not None: self.bridge.activate_scene('0', self.station_id_2) self.nuimo.display_matrix(matrices.STATION2) def on_longtouch_right(self): logger.debug("on_longtouch_right()") if self.station_id_3 is not None: self.bridge.activate_scene('0', self.station_id_3) self.nuimo.display_matrix(matrices.STATION3) def set_light_attributes(self, **attributes): response = self.lights.set_attributes(attributes) if 'errors' in response: logger.error("Failed to set light attributes: %s", response['errors']) self.nuimo.display_matrix(matrices.ERROR) return if 'xy' in attributes: if 'bri' in attributes: self.nuimo.display_matrix(matrices.LETTER_W) else: self.nuimo.display_matrix(matrices.SHUFFLE) elif 'on' in attributes and not ('bri_inc' in attributes): if self.lights.on: self.nuimo.display_matrix(matrices.LIGHT_ON) else: self.nuimo.display_matrix(matrices.LIGHT_OFF) elif 'bri' in attributes or 'bri_inc' in attributes: if self.lights.brightness: matrix = matrices.progress_bar(self.lights.brightness / self.delta_range.stop) self.nuimo.display_matrix(matrix, fading=True, ignore_duplicates=True) else: self.set_light_attributes(on=False) def on_swipe_left(self): self.set_light_attributes(on=True, bri=self.lights.brightness, xy=COLOR_WHITE_XY) def on_swipe_right(self): self.set_light_attributes(on=True, xy=(random(), random())) def on_rotation(self, value): self.delta += value def run(self): prev_sync_time = time() prev_update_time = time() while not self.stopped: now = time() if self.delta and now - prev_update_time >= self.lights.update_interval: self.send_updates() self.delta = 0 prev_update_time = now if now - max([prev_sync_time, prev_update_time ]) >= self.lights.sync_interval: self.lights.update_state() prev_sync_time = now sleep(0.05) def send_updates(self): delta = round( clamp_value(self.delta_range.stop * self.delta, self.delta_range)) if self.lights.on: self.set_light_attributes(bri_inc=delta) else: if delta > 0: self.set_light_attributes(on=True) self.set_light_attributes(bri_inc=delta)
class HueUtility: @staticmethod def connect(ip): Bridge(ip=ip) DEFAULT_LAMP_ID = 1 def __init__(self, lamp_id): self.logger = logging.getLogger('hue_utility') self.logger.setLevel(logging.INFO) self.bridge = Bridge() # LAMP_NAME = "Лампа" # lights = self.bridge.get_light_objects(mode="name") # self.light = lights[LAMP_NAME] self.light = Light(self.bridge, lamp_id) def on(self): self.logger.debug("on") self.light.on = True def off(self): self.logger.debug("off") self.light.on = False def is_on(self): self.logger.debug("is_on") return self.light.on is True def brightness(self, brightness): self.logger.debug(f"brightness={brightness}") if brightness == 0: self.off() return real_brightness = 254 * brightness // 100 if not self.is_on(): self.on() self.light.brightness = real_brightness def hue(self, hue): self.logger.debug(f"hue={hue}") self.light.hue = hue def saturation(self, saturation): self.logger.debug(f"saturation={saturation}") self.light.saturation = saturation def temperature(self, temperature): self.logger.debug(f"temperature={temperature}") self.light.colortemp_k = temperature def xy(self, xy): self.logger.debug(f"xy={xy}") self.light.xy = xy def hsv(self, hsv): self.logger.debug(f"hsv={hsv}") self.hue(hsv[0]) self.saturation(hsv[1]) def scene(self, name): self.logger.debug(f"scene={name}") scenes = self.bridge.scenes for scene in scenes: if scene.name == name: group = AllLights() self.logger.debug(f"scene id={scene.scene_id}") self.logger.debug(f"group id={group.group_id}") self.bridge.activate_scene(group.group_id, scene.scene_id, transition_time=1) return self.logger.error(f"Scene {name} not found") def alert(self, alert_command): self.logger.debug(f"alert={alert_command}") if alert_command == "single": self.light.alert = "select" elif alert_command == "start": self.light.alert = "lselect" elif alert_command == "stop": self.light.alert = "none" def effect(self, effect_command): self.logger.debug(f"effect={effect_command}") if effect_command == "start": self.light.effect = "colorloop" elif effect_command == "stop": self.light.effect = "none"
class PhillipsHueSkill(MycroftSkill): def __init__(self): super(PhillipsHueSkill, self).__init__(name="PhillipsHueSkill") self.brightness_step = int(self.settings.get('brightness_step', DEFAULT_BRIGHTNESS_STEP)) self.color_temperature_step = \ int(self.settings.get('color_temperature_step', DEFAULT_COLOR_TEMPERATURE_STEP)) verbose = self.settings.get('verbose', False) if type(verbose) == str: verbose = verbose.lower() verbose = True if verbose == 'true' else False self.verbose = verbose converter = Converter() self.colors = { "red": (65160, 254), "green": (27975, 254), "blue": (45908, 254), "pink": (52673, 254), "violet": (48156, 254), "yellow": (10821, 254), "orange": ( 6308, 254), "white": (41439, 81), } self.username = self.settings.get('username') if self.username == '': self.username = None self.ip = None # set in _connect_to_bridge self.bridge = None self.default_group = None self.groups_to_ids_map = dict() self.scenes_to_ids_map = defaultdict(dict) @property def connected(self): return self.bridge is not None @property def user_supplied_ip(self): return self.settings.get('ip') != '' @property def user_supplied_username(self): return self.settings.get('username') != '' def _register_with_bridge(self): """ Helper for connecting to the bridge. If we don't have a valid username for the bridge (ip) we are trying to use, this will cause one to be generated. """ self.speak_dialog('connect.to.bridge') i = 0 while i < 30: sleep(1) try: self.bridge = Bridge(self.ip) except PhueRegistrationException: continue else: break if not self.connected: self.speak_dialog('failed.to.register') else: self.speak_dialog('successfully.registered') def _update_bridge_data(self): """ This should be called any time a successful connection is established. It sets some member variables, and ensures that scenes and groups are registered as vocab. """ self.username = self.bridge.username with self.file_system.open('username', 'w') as conf_file: conf_file.write(self.username) if not self.default_group: self._set_default_group(self.settings.get('default_group')) self._register_groups_and_scenes() def _attempt_connection(self): """ This will attempt to connect to the bridge, but will not handle any errors on it's own. Raises ------ UnauthorizedUserException If self.username is not None, and is not registered with the bridge """ if self.user_supplied_ip: self.ip = self.settings.get('ip') else: device = next(filter(lambda device : "Philips hue" in device.model_name, upnpclient.discover())) self.ip = urllib.parse.urlparse(device.location).hostname if self.username: url = 'http://{ip}/api/{user}'.format(ip=self.ip, user=self.username) data = get(url).json() data = data[0] if isinstance(data, list) else data error = data.get('error') if error: description = error.get('description') if description == "unauthorized user": raise UnauthorizedUserException(self.username) else: raise Exception('Unknown Error: {0}'.format(description)) self.bridge = Bridge(self.ip, self.username) def _connect_to_bridge(self, acknowledge_successful_connection=False): """ Calls _attempt_connection, handling various exceptions by either alerting the user to issues with the config/setup, or registering the application with the bridge. Parameters ---------- acknowledge_successful_connection : bool Speak when a successful connection is established. Returns ------- bool True if a connection is established. """ try: self._attempt_connection() except DeviceNotFoundException: self.speak_dialog('bridge.not.found') return False except ConnectionError: self.speak_dialog('failed.to.connect') if self.user_supplied_ip: self.speak_dialog('ip.in.config') return False except socket.error as e: if 'No route to host' in e.args: self.speak_dialog('no.route') else: self.speak_dialog('failed.to.connect') return False except UnauthorizedUserException: if self.user_supplied_username: self.speak_dialog('invalid.user') return False else: self._register_with_bridge() except PhueRegistrationException: self._register_with_bridge() if not self.connected: return False if acknowledge_successful_connection: self.speak_dialog('successfully.connected') self._update_bridge_data() return True def _set_default_group(self, identifier): """ Sets the group to which commands will be applied, when a group is not specified in the command. Parameters ---------- identifier : str or int The name of the group, or it's integer id Notes ----- If the group does not exist, 0 (all lights) will be used. """ try: self.default_group = Group(self.bridge, identifier) except LookupError: self.speak_dialog('could.not.find.group', {'name': identifier}) self.speak_dialog('using.group.0') self.default_group = Group(self.bridge, 0) def _register_groups_and_scenes(self): """ Register group and scene names as vocab, and update our caches. """ groups = self.bridge.get_group() for id, group in groups.items(): name = group['name'].lower() self.groups_to_ids_map[name] = id self.register_vocabulary(name, "Group") scenes = self.bridge.get_scene() for id, scene in scenes.items(): name = scene['name'].lower() group_id = scene.get('group') group_id = int(group_id) if group_id else None self.scenes_to_ids_map[group_id][name] = id self.register_vocabulary(name, "Scene") def initialize(self): """ Attempt to connect to the bridge, and build/register intents. """ self.load_data_files(dirname(__file__)) if self.file_system.exists('username'): if not self.user_supplied_username: with self.file_system.open('username', 'r') as conf_file: self.username = conf_file.read().strip(' \n') try: self._attempt_connection() self._update_bridge_data() except (PhueRegistrationException, DeviceNotFoundException, UnauthorizedUserException, ConnectionError, socket.error): # Swallow it for now; _connect_to_bridge will deal with it pass self.register_intent_file("turn.on.intent", self.handle_turn_on_intent) self.register_intent_file("turn.off.intent", self.handle_turn_off_intent) self.register_intent_file("set.lights.intent", self.handle_set_lights_brightness_intent) self.register_intent_file("set.lights.scene.intent", self.handle_set_lights_scene_intent) self.register_intent_file("set.lights.color.intent", self.handle_set_lights_color_intent) # adjust_brightness_intent = IntentBuilder("AdjustBrightnessIntent") \ # .one_of("IncreaseKeyword", "DecreaseKeyword", "DimKeyword") \ # .one_of("Group", "LightsKeyword") \ # .optionally("BrightnessKeyword") \ # .build() # self.register_intent(adjust_brightness_intent, # self.handle_adjust_brightness_intent) # adjust_color_temperature_intent = \ # IntentBuilder("AdjustColorTemperatureIntent") \ # .one_of("IncreaseKeyword", "DecreaseKeyword") \ # .one_of("Group", "LightsKeyword") \ # .require("ColorTemperatureKeyword") \ # .build() # self.register_intent(adjust_color_temperature_intent, # self.handle_adjust_color_temperature_intent) # connect_lights_intent = \ # IntentBuilder("ConnectLightsIntent") \ # .require("ConnectKeyword") \ # .one_of("Group", "LightsKeyword") \ # .build() # self.register_intent(connect_lights_intent, # self.handle_connect_lights_intent) def _find_fuzzy(self, dictionary, value): result = process.extractOne(value, dictionary.keys()) if result is None: return None (value, confidence) = result if confidence < 60: return None else: return dictionary[value] def _find_group(self, group_name): group_id = self._find_fuzzy(self.groups_to_ids_map, group_name) if group_id is not None: return Group(self.bridge, group_id) @get_group def handle_turn_on_intent(self, message, group): group.on = True @get_group def handle_turn_off_intent(self, message, group): group.on = False @get_group def handle_set_lights_brightness_intent(self, message, group): value = message.data.get('percent') value = int(value.rstrip('%')) if value == 0: group.on = False else: brightness = int(value / 100.0 * 254) group.on = True group.brightness = brightness if self.verbose: self.speak_dialog('set.brightness', {'brightness': value}) @get_group def handle_set_lights_scene_intent(self, message, group): scene_name = message.data.get('scene') scene_id = self._find_fuzzy(self.scenes_to_ids_map[group.group_id], scene_name) if not scene_id: scene_id = self._find_fuzzy(self.scenes_to_ids_map[None], scene_name) if scene_id: if self.verbose: self.speak_dialog('activate.scene', {'scene': scene_name}) self.bridge.activate_scene(group.group_id, scene_id) else: self.speak_dialog('scene.not.found', {'scene': scene_name}) @get_group def handle_set_lights_color_intent(self, message, group): color_name = message.data.get('color') (hue, sat) = self.colors[color_name] for light in group.lights: light.on = True light.hue = hue light.saturation = sat # TODO support # @intent_handler # def handle_adjust_brightness_intent(self, message, group): # if "IncreaseKeyword" in message.data: # brightness = group.brightness + self.brightness_step # group.brightness = \ # brightness if brightness < 255 else 254 # dialog = 'increase.brightness' # else: # brightness = group.brightness - self.brightness_step # group.brightness = brightness if brightness > 0 else 0 # dialog = 'decrease.brightness' # if self.verbose: # self.speak_dialog(dialog) # TODO support # @intent_handler # def handle_adjust_color_temperature_intent(self, message, group): # if "IncreaseKeyword" in message.data: # color_temperature = \ # group.colortemp_k + self.color_temperature_step # group.colortemp_k = \ # color_temperature if color_temperature < 6500 else 6500 # dialog = 'increase.color.temperature' # else: # color_temperature = \ # group.colortemp_k - self.color_temperature_step # group.colortemp_k = \ # color_temperature if color_temperature > 2000 else 2000 # dialog = 'decrease.color.temperature' # if self.verbose: # self.speak_dialog(dialog) # TODO support # @intent_handler # def handle_connect_lights_intent(self, message, group): # if self.user_supplied_ip: # self.speak_dialog('ip.in.config') # return # if self.verbose: # self.speak_dialog('connecting') # self._connect_to_bridge(acknowledge_successful_connection=True) def stop(self): pass
class LightBot(Plugin): # Which lights should be targeted if no light specifying parameter is provided? all_lights = [0] def __init__(self, name=None, slack_client=None, plugin_config=None): super(LightBot, self).__init__(name=name, slack_client=slack_client, plugin_config=plugin_config) bridge_address = plugin_config.get('HUE_BRIDGE_ADDRESS', None) self.allowed_light_control_channel_ids = plugin_config.get('CHANNELS', None) self.allowed_light_control_user_ids = plugin_config.get('USERS', None) self.wootric_bot_id = plugin_config.get('WOOTRIC_BOT', None) self.wigwag_color = self.xy_from_color_string(plugin_config.get('WIGWAG_COLOR', str(DEFAULT_WIGWAG_COLOR))) self.whirl_color = self.xy_from_color_string(plugin_config.get('WHIRL_COLOR', str(DEFAULT_WHIRL_COLOR))) self.slow_pulse_color = self.xy_from_color_string( plugin_config.get('SLOW_PULSE_COLOR', str(DEFAULT_SLOW_PULSE_COLOR))) self.slow_pulse_lights = plugin_config.get('SLOW_PULSE_LIGHTS', None) config_lights = plugin_config.get('LIGHTS', None) if config_lights is not None: self.all_lights = config_lights if not bridge_address: raise ValueError("Please add HUE_BRIDGE_ADDRESS under LightBot in your config file.") self.bridge = Bridge(bridge_address) self.bridge.connect() if self.debug: print dumps(self.bridge.get_api()) lights_on_bridge = self.bridge.lights if self.all_lights == [0]: # The magic 0 light ID does not work for most light settings we will use self.all_lights = [] for light in lights_on_bridge: self.all_lights.append(light.light_id) config_wig_wag_groups = plugin_config.get('WIGWAG_GROUPS', None) if config_wig_wag_groups is not None and len(config_wig_wag_groups) == 2 \ and len(config_wig_wag_groups[0]) > 0 and len(config_wig_wag_groups[1]): self.wigwag_groups = config_wig_wag_groups else: self.wigwag_groups = None if self.slow_pulse_lights is None: self.slow_pulse_lights = self.all_lights # Check for whirl lights, either as an array of IDs or an array of arrays of IDs. config_whirl_lights = plugin_config.get('WHIRL_LIGHTS', None) self.whirl_lights = [] if config_whirl_lights is None: # Default to all lights for light_id in self.all_lights: # Put each light ID into an array since whirl_lights is an array of arrays of IDs. self.whirl_lights.append([light_id]) else: for whirl_group in config_whirl_lights: if isinstance(whirl_group, list): # The user gave us an array of arrays, just what we wanted! :x self.whirl_lights.append(whirl_group) elif whirl_group is not None: # It's not an array and not None. This is probably a single light ID scalar. self.whirl_lights.append([whirl_group]) if self.wigwag_groups is None: # We do not have configuration-specified wig wag groups. Use all odd and even lights. even_lights = [] odd_lights = [] for light in lights_on_bridge: if light.light_id % 2 == 0: even_lights.append(light.light_id) else: odd_lights.append(light.light_id) self.wigwag_groups = [odd_lights, even_lights] def process_message(self, data): print dumps(data) is_wootric_bot = ('subtype' in data and data['subtype'] == 'bot_message' and 'bot_id' in data and data['bot_id'] == self.wootric_bot_id) user_impersonating_bot = self.debug and 'user' in data and data['user'] in self.allowed_light_control_user_ids light_control_regex = r"(?i)^lights?\s+(\S+.*)$" # Direct light control if self.message_allows_light_control(data): # Match any command beginning with "lights" and containing any other command(s) pattern = re.compile(light_control_regex) match = pattern.match(data['text']) if match is not None: light_command = match.group(1) if light_command is not None: self.process_lights_command(light_command, data) # NPS scores if is_wootric_bot or user_impersonating_bot: pattern = re.compile(r"[*_]*New NPS rating:\s+(\d+).*") if user_impersonating_bot: match = pattern.match(data['text']) elif is_wootric_bot and 'attachments' in data: match = pattern.match(data['attachments'][0]['text']) else: match = None if match is not None: nps_score = match.group(1) if nps_score is not None: self.process_nps_score(nps_score) def process_lights_command(self, args, data=None): pattern = re.compile(r"(?i)^((\d+\s+)+)?([#\S]+.*%?)$") match = pattern.match(args) if match is not None and match.group(1) is not None: target_lights = match.group(1).split() else: target_lights = self.all_lights command = match.group(3) if 'debug' in command.lower(): self.handle_debug_command(args, data) return if command.lower() == 'whirl': self.whirl() return if command.lower() == 'wigwag': self.wigwag() return if command.lower() == 'pulsate': self.pulsate() return if command.lower() == 'on': self.lights_on_or_off(True, target_lights) return elif command.lower() == 'off': self.lights_on_or_off(False, target_lights) return if command.lower() == 'dance party': self.dance_party(target_lights) return # Check for a color try: xy = self.xy_from_color_string(command) if xy is not None: self.color_change(xy, target_lights) return except ValueError: pass # Check for brightness pattern = re.compile(r"(?i)^bri(ghtness)?\s+(\d+(%?|(\.\d+)?))$") match = pattern.match(command) if match is not None: brightness = match.group(2) if brightness is not None: self.brightness_change(brightness, target_lights) return # Check for a scene after updating Hue API scene_id = self.scene_id_matching_string(command) if scene_id is not None: self.bridge.activate_scene(0, scene_id) return def handle_debug_command(self, command, incoming_data=None): if command == 'debug rules': data_type = 'rules' data = self.bridge.request('GET', '/api/' + self.bridge.username + '/rules') elif command == 'debug schedules': data_type = 'schedules' data = self.bridge.get_schedule() elif command == 'debug lights': data_type = 'lights' data = self.bridge.get_light() elif command == 'debug sensors': data_type = 'sensors' data = self.bridge.get_sensor() else: data_type = 'bridge objects of all types' data = self.bridge.get_api() pretty_data_string = dumps(data, sort_keys=True, indent=4, separators=(',',':')) message_attachments = [{ 'fallback': '%d %s:' % (len(data), data_type), 'title': '%d %s:' % (len(data), data_type), 'text': pretty_data_string }] self.slack_client.api_call('chat.postMessage', as_user=True, channel=incoming_data['channel'], attachments=message_attachments, type='message') def scene_id_matching_string(self, scene_name): name = scene_name.lower() for scene_id, scene in self.bridge.get_scene().iteritems(): if scene['name'].lower() == name: return scene_id return None # Disables all enabled schedules for the time period specified def disable_schedules_for_time(self, seconds): if seconds < 1: seconds = 1 seconds = int(ceil(seconds)) minutes = seconds / 60 seconds %= 60 hours = minutes / 60 minutes %= 60 time_string = 'PT%02d:%02d:%02d' % (hours, minutes, seconds) all_schedules = self.bridge.get_schedule() for schedule_id, schedule in all_schedules.iteritems(): if schedule['status'] == 'enabled': reenable_schedule_schedule = { 'name': 'temporarilyDisableSchedule%s' % str(schedule_id), 'time': time_string, 'command': { 'method': 'PUT', 'address': '/api/' + self.bridge.username + '/schedules/' + str(schedule_id), 'body': {'status': 'enabled'} } } result = self.bridge.request('PUT', '/api/' + self.bridge.username + '/schedules/' + str(schedule_id), dumps({'status': 'disabled'})) self.bridge.request('POST', '/api/' + self.bridge.username + '/schedules', dumps(reenable_schedule_schedule)) print result # Accepts colors in the format of a color name, XY values, RGB values, or hex RGB code. # Returns [X,Y] for use in the Philips Hue API. def xy_from_color_string(self, string): # Our regex patterns hex_pattern = re.compile(r"^#?(([A-Fa-f\d]{3}){1,2})$") xy_pattern = re.compile(r"^[[({]?\s*(\d+(\.\d+)?)[,\s]+(\d+(\.\d+)?)\s*[])}]?\s*$") rgb_integer_pattern = re.compile(r"^[[({]?\s*(\d+)[,\s]+(\d+(\.\d+)?)[,\s]+(\d+)\s*[])}]?\s*$") rgb_percent_pattern = re.compile(r"^[[({]?\s*(\d+)%[,\s]+(\d+)%[,\s]+(\d+)%\s*[])}]?\s*$") rgb = None xy = None try: rgb = name_to_rgb(string) except ValueError: pass if rgb is None: # No name matched match = hex_pattern.match(string) if match is not None: try: rgb = hex_to_rgb("#" + match.group(1)) except ValueError: pass else: # No name, no hex match = rgb_percent_pattern.match(string) r = None g = None b = None if match is not None: r = int(match.group(1)) * 255 / 100 g = int(match.group(2)) * 255 / 100 b = int(match.group(3)) * 255 / 100 else: # No name, no hex, no RGB percent match = rgb_integer_pattern.match(string) if match is not None: r = int(match.group(1)) g = int(match.group(2)) b = int(match.group(4)) if r is not None and g is not None and b is not None: rgb = [r, g, b] else: # No name, no hex, no RGB percent, no RGB integers match = xy_pattern.match(string) if match is not None: xy = [float(match.group(1)), float(match.group(3))] if xy is None and rgb is not None: # We have RGB. Convert to XY for Philips-ness. xy = self.rgb_to_xy(rgb) return xy @staticmethod def rgb_to_xy(rgb): # Some magic number witchcraft to go from rgb 255 to Philips XY # from http://www.developers.meethue.com/documentation/color-conversions-rgb-xy red = rgb[0] / 255.0 green = rgb[1] / 255.0 blue = rgb[2] / 255.0 red = ((red + 0.055) / (1.0 + 0.055) ** 2.4) if (red > 0.04045) else (red / 12.92) green = ((green + 0.055) / (1.0 + 0.055) ** 2.4) if (green > 0.04045) else (green / 12.92) blue = ((blue + 0.055) / (1.0 + 0.055) ** 2.4) if (blue > 0.04045) else (blue / 12.92) x = red * 0.664511 + green * 0.154324 + blue * 0.162028 y = red * 0.283881 + green * 0.668433 + blue * 0.047685 z = red * 0.000088 + green * 0.072310 + blue * 0.986039 return [x / (x+y+z), y / (x+y+z)] def color_change(self, xy, lights): for light in lights: self.bridge.set_light(int(light), {'on': True, 'xy': xy}) def brightness_change(self, brightness, lights): if '%' in brightness: brightness = brightness.strip('%') brightness = float(brightness) / 100.0 brightness = int(brightness * 255.0) else: brightness = float(brightness) if brightness == 1: brightness = 255 elif 0.0 < brightness < 1.0: brightness = int(brightness * 255) for light in lights: self.bridge.set_light(int(light), {'on': True, 'bri': brightness}) def message_allows_light_control(self, data): # Is this person in one of our full control channels? if 'channel' in data: if (self.allowed_light_control_channel_ids is None or data['channel'] in self.allowed_light_control_channel_ids): return True if 'user' in data and data['user'] in self.allowed_light_control_user_ids: return True return False def process_nps_score(self, score): if score == '10': self.whirl() elif score == '9': self.wigwag() elif score == '0': self.pulsate() def lights_on_or_off(self, off_or_on, lights): for light in lights: self.bridge.set_light(int(light), {'on': off_or_on}) def dance_party(self, lights): starting_status = {} flash_count = 66 delay_between_flashes = 0.15 total_duration = flash_count * delay_between_flashes self.disable_schedules_for_time(total_duration) for light in lights: state = self.bridge.get_light(int(light))['state'] if state is not None: starting_status[light] = self.restorable_state_for_light(state) for i in lights: self.bridge.set_light(int(i), {'on': True}) for loop_index in range(0,flash_count): for i in lights: xy = [random.uniform(0.0, 1.0), random.uniform(0.0, 1.0)] self.bridge.set_light(int(i), {'bri': 250, 'xy': xy, 'transitionTime': 0, 'alert': 'select'}) time.sleep(delay_between_flashes) for light in lights: self.bridge.set_light(int(light), starting_status[light]) @staticmethod def restorable_state_for_light(light_object): state = {'bri': light_object['bri'], 'on': light_object['on']} if light_object['colormode'] == 'hs': state['hue'] = light_object['hue'] state['sat'] = light_object['sat'] elif light_object['colormode'] == 'ct': state['ct'] = light_object['ct'] else: state['xy'] = light_object['xy'] return state def wigwag(self): starting_status = {} all_wigwag_lights = self.wigwag_groups[0] + self.wigwag_groups[1] transition_time = 5 in_one_second = 'PT00:00:01' repeat_count = 5 seconds_between_phases = 1 every_two_seconds = 'R%02d/PT00:00:02' % repeat_count total_duration = repeat_count * seconds_between_phases * 2 after_its_over = 'PT00:00:%02d' % total_duration self.disable_schedules_for_time(total_duration) for light_id in all_wigwag_lights: state = self.bridge.get_light(int(light_id))['state'] if state is not None: starting_status[light_id] = self.restorable_state_for_light(state) if not state['on']: self.bridge.create_schedule('turn%dOnBeforeWigwag' % light_id, in_one_second, light_id, {'on': True}) # Ensure all lights will be on for light_id in all_wigwag_lights: if not self.bridge.lights_by_id[light_id].on: self.bridge.create_schedule('turn%dOnBeforeWigwag' % light_id, in_one_second, light_id, {'on': True}) # First phase for light_id in self.wigwag_groups[0]: self.bridge.create_schedule('wigwag-1-%d' % light_id, every_two_seconds, light_id, { 'xy': self.wigwag_color, 'bri': 154, 'transitiontime': transition_time }) for light_id in self.wigwag_groups[1]: self.bridge.create_schedule('wigwag-1-%d' % light_id, every_two_seconds, light_id, { 'xy': self.wigwag_color, 'bri': 0, 'transitiontime': transition_time }) # Delay before setting second phase time.sleep(seconds_between_phases) # Second phase for light_id in self.wigwag_groups[0]: self.bridge.create_schedule('wigwag-2-%d' % light_id, every_two_seconds, light_id, { 'xy': self.wigwag_color, 'bri': 0, 'transitiontime': transition_time }) for light_id in self.wigwag_groups[1]: self.bridge.create_schedule('wigwag-2-%d' % light_id, every_two_seconds, light_id, { 'xy': self.wigwag_color, 'bri': 154, 'transitiontime': transition_time }) # Restore original state for light_id in all_wigwag_lights: result = self.bridge.create_schedule('wigwag-3-%d' % light_id, after_its_over, light_id, starting_status[light_id]) if self.debug: print "Setting light %d to restore state to:\n" % light_id print starting_status[light_id] print result if self.debug: print self.bridge.get_schedule() def delete_all_sensors_with_name_begining(self, name_prefix): all_sensors = self.bridge.get_sensor() for sensor_id, sensor in all_sensors.iteritems(): if name_prefix in sensor['name']: result = self.bridge.request('DELETE', '/api/' + self.bridge.username + '/sensors/' + str(sensor_id)) print result def delete_all_schedules_with_name_begining(self, name_prefix): all_schedules = self.bridge.request('GET', '/api/' + self.bridge.username + '/schedules') for schedule_id, schedule in all_schedules.iteritems(): if name_prefix in schedule['name']: self.bridge.request('DELETE', '/api/' + self.bridge.username + '/schedules/' + str(schedule_id)) def delete_all_rules_with_name_begining(self, name_prefix): all_rules = self.bridge.request('GET', '/api/' + self.bridge.username + '/rules') for rule_id, rule in all_rules.iteritems(): if name_prefix in rule['name']: self.bridge.request('DELETE', '/api/' + self.bridge.username + '/rules/' + str(rule_id)) def pulsate(self): lights = self.all_lights starting_status = {} # More than seven lights would require multiple rules in the Bridge since we are limited to 8 actions per rule. # This would be relatively straight forward to solve but is not worth the effort at the moment. if len(lights) > 6: print '%d lights are specified to pulsate.' % len(lights)\ + 'Only pulsating up to 6 is currently supported.' \ + 'List will be truncated to 6.' lights = lights[:6] pulse_bri = 88 original_fade_duration_deciseconds = 50 original_fade_schedule_time = 'PT00:00:%02d' % (original_fade_duration_deciseconds / 10) half_pulse_duration_deciseconds = 20 half_pulse_schedule_time = 'PT00:00:%02d' % (half_pulse_duration_deciseconds / 10) pulse_count = 5 total_duration_seconds = pulse_count * half_pulse_duration_deciseconds * 2 / 10 total_duration_minutes = total_duration_seconds / 60 total_duration_seconds %= 60 total_duration_schedule_time = 'PT00:%02d:%02d' % (total_duration_minutes, total_duration_seconds) self.delete_all_sensors_with_name_begining('Pulsation') self.delete_all_schedules_with_name_begining('Pulsation') self.delete_all_rules_with_name_begining('Pulsation') self.disable_schedules_for_time(total_duration_seconds) for light in lights: state = self.bridge.get_light(int(light))['state'] if state is not None: restorable_state = self.restorable_state_for_light(state) restorable_state['transitiontime'] = 20 starting_status[light] = restorable_state lights_up_state = { 'bri': pulse_bri, 'xy': self.slow_pulse_color, 'transitiontime': half_pulse_duration_deciseconds } lights_down_state = { 'bri': 0, 'xy': self.slow_pulse_color, 'transitiontime': half_pulse_duration_deciseconds } pulsation_status_sensor = { 'name': 'PulsationStatusSensor', 'uniqueid': 'PulsationStatusSensor', 'type': 'CLIPGenericStatus', 'swversion': '1.0', 'state': { 'status': 0 }, 'manufacturername': 'jasonneel', 'modelid': 'PulsationStatusSensor' } # Create the sensors used for status result = self.bridge.request('POST', '/api/' + self.bridge.username + '/sensors', dumps(pulsation_status_sensor)) status_sensor_id = result[0]['success']['id'] pulsation_state_address = '/sensors/' + str(status_sensor_id) + '/state' # Schedules going_up_schedule = { 'name': 'Pulsation going up', 'time': half_pulse_schedule_time, 'autodelete': False, 'status': 'disabled', 'command': { 'address': '/api/' + self.bridge.username + pulsation_state_address, 'method': 'PUT', 'body': { 'status': PULSATION_SENSOR_STATE_GOING_DOWN } } } going_down_schedule = { 'name': 'Pulsation going down', 'time': half_pulse_schedule_time, 'autodelete': False, 'status': 'disabled', 'command': { 'address': '/api/' + self.bridge.username + pulsation_state_address, 'method': 'PUT', 'body': { 'status': PULSATION_SENSOR_STATE_GOING_UP } } } going_up_result = self.bridge.request('POST', '/api/' + self.bridge.username + '/schedules', dumps(going_up_schedule)) going_up_schedule_id = going_up_result[0]['success']['id'] going_down_result = self.bridge.request('POST', '/api/' + self.bridge.username + '/schedules', dumps(going_down_schedule)) going_down_schedule_id = going_down_result[0]['success']['id'] # Create the two rules for going up and down start_going_up_rule = { 'name': 'Pulsation at bottom', 'conditions': [ { 'address': pulsation_state_address + '/status', 'operator': 'eq', 'value': str(PULSATION_SENSOR_STATE_GOING_UP) }, { 'address': pulsation_state_address + '/lastupdated', 'operator': 'dx' } ], 'actions': [ { 'address': '/schedules/' + str(going_up_schedule_id), 'method': 'PUT', 'body': {'status': 'enabled'} }, { 'address': '/schedules/' + str(going_down_schedule_id), 'method': 'PUT', 'body': {'status': 'disabled'} } ] } start_going_down_rule = { 'name': 'Pulsation at top', 'conditions': [ { 'address': pulsation_state_address + '/status', 'operator': 'eq', 'value': str(PULSATION_SENSOR_STATE_GOING_DOWN) }, { 'address': pulsation_state_address + '/lastupdated', 'operator': 'dx' } ], 'actions': [ { 'address': '/schedules/' + str(going_up_schedule_id), 'method': 'PUT', 'body': {'status': 'disabled'} }, { 'address': '/schedules/' + str(going_down_schedule_id), 'method': 'PUT', 'body': {'status': 'enabled'} } ] } original_light_state_rule = { 'name': 'Pulsation restore state', 'conditions': [ { 'address': pulsation_state_address + '/status', 'operator': 'eq', 'value': str(PULSATION_SENSOR_STATE_CLEANUP) }, { 'address': pulsation_state_address + '/lastupdated', 'operator': 'dx' } ], 'actions': [] } # Add actions to both rules to bring each light up and down as the sensor state changes for light_id in lights: light_address = '/lights/' + str(light_id) + '/state' start_going_up_rule['actions'].append({ 'address': light_address, 'method': 'PUT', 'body': lights_up_state }) start_going_down_rule['actions'].append({ 'address': light_address, 'method': 'PUT', 'body': lights_down_state }) original_light_state_rule['actions'].append({ 'address': light_address, 'method': 'PUT', 'body': starting_status[light_id] }) going_up_result = self.bridge.request('POST', '/api/' + self.bridge.username + '/rules', dumps(start_going_up_rule)) going_up_rule_id = going_up_result[0]['success']['id'] going_down_result = self.bridge.request('POST', '/api/' + self.bridge.username + '/rules', dumps(start_going_down_rule)) going_down_rule_id = going_down_result[0]['success']['id'] result = self.bridge.request('POST', '/api/' + self.bridge.username + '/rules', dumps(original_light_state_rule)) cleanup_rule = { 'name': 'Pulsation clean up', 'conditions': [ { 'address': pulsation_state_address + '/status', 'operator': 'eq', 'value': str(PULSATION_SENSOR_STATE_CLEANUP) }, { 'address': pulsation_state_address + '/lastupdated', 'operator': 'dx' } ], 'actions': [ { 'address': '/rules/' + str(going_up_rule_id), 'method': 'PUT', 'body': {'status': 'disabled'} }, { 'address': '/rules/' + str(going_down_rule_id), 'method': 'PUT', 'body': {'status': 'disabled'} }, { 'address': '/schedules/' + str(going_up_schedule_id), 'method': 'PUT', 'body': {'status': 'disabled'} }, { 'address': '/schedules/' + str(going_down_schedule_id), 'method': 'PUT', 'body': {'status': 'disabled'} } ] } result = self.bridge.request('POST', '/api/' + self.bridge.username + '/rules', dumps(cleanup_rule)) # The schedule that stops the constant cleanup_schedule = { 'name': 'Pulsation clean up', 'time': total_duration_schedule_time, 'command': { 'address': '/api/' + self.bridge.username + pulsation_state_address, 'method': 'PUT', 'body': { 'status': PULSATION_SENSOR_STATE_CLEANUP } } } result = self.bridge.request('POST', '/api/' + self.bridge.username + '/schedules', dumps(cleanup_schedule)) # First fade them all down to nothing lights_totally_off = { 'bri': 0, 'transitiontime': original_fade_duration_deciseconds } for light_id in lights: light = self.bridge.lights_by_id[light_id] result = self.bridge.request('PUT', '/api/' + self.bridge.username + '/lights/' + str(light_id) + '/state', dumps(lights_totally_off)) print result # Start the pulsation once that is done begin_schedule = { 'name': 'Pulsation begin', 'time': original_fade_schedule_time, 'command': { 'address': '/api/' + self.bridge.username + pulsation_state_address, 'method': 'PUT', 'body': { 'status': PULSATION_SENSOR_STATE_GOING_UP } } } result = self.bridge.request('POST', '/api/' + self.bridge.username + '/schedules', dumps(begin_schedule)) def whirl(self): starting_status = {} # Flatten our array of arrays of light IDs to save the starting states flattened_whirl_light_ids = list(itertools.chain.from_iterable(self.whirl_lights)) for light_id in flattened_whirl_light_ids: state = self.bridge.get_light(int(light_id))['state'] if state is not None: starting_status[light_id] = self.restorable_state_for_light(state) step_time = 0.075 time_between_whirls = 0.5 transition_time = 1 whirl_count = 10 total_seconds = ((step_time * len(self.whirl_lights)) + time_between_whirls) * whirl_count finished_timestamp = 'PT00:00:%02d' % total_seconds self.disable_schedules_for_time(total_seconds) # Return to original state after we're done for light_id in flattened_whirl_light_ids: self.bridge.create_schedule('restore%dAfterWhirl' % light_id, finished_timestamp, light_id, starting_status[light_id]) # Build our 'off' states to go with the on state off_states = {} for light_id, status in starting_status.iteritems(): state = deepcopy(status) del state['on'] if not status['on']: state['bri'] = 0 off_states[light_id] = state # New hotness: on_state = {'xy': self.whirl_color, 'bri': 255, 'transitiontime': transition_time} on_state_plus_on = on_state.copy() on_state_plus_on['on'] = True for whirl_index in range(0,whirl_count): for group_index in range(0,len(self.whirl_lights)+2): coming_down_index = group_index - 2 if group_index < len(self.whirl_lights): state = on_state if whirl_index != 0 else on_state_plus_on for light_id in self.whirl_lights[group_index]: self.bridge.set_light(light_id, state) if coming_down_index >= 0: for light_id in self.whirl_lights[coming_down_index]: self.bridge.set_light(light_id, off_states[light_id]) time.sleep(step_time) time.sleep(time_between_whirls)
class Client: def __init__(self, ip): self.ip = ip self.bridge = Bridge(self.ip) self.lights = self.bridge.lights self.scenes = self.bridge.scenes self.groups = self.bridge.groups # Internal variables self._last_lights = [] self._alarm_lights = [] self._alarm_lights_state = [] self._alarm_active = False def check_scene_active(self, scene): for id in scene['lights']: if scene['lights'][id]['state']: # Light should be on, verify that it is if self.lights[id].on: # Light is on, verify xy brightness and color (xy) if not scene['lights'][id]['brightness'] == self.lights[id].brightness or \ not self._compare_xy(scene['lights'][id]['xy'], self.lights[id].xy, scene['xy_tolerance']): return False else: return False else: # Light should be off, verify that it is if self.lights[id].on: return False return True def set_scene(self, name): if name == 'All off': self.set_all_off() elif name == 'Not home': self.bridge.activate_scene(self._get_group_id_by_name('Woonkamer'), self._get_scene_id_by_name(name)) def get_light_changes(self): lights = [] for light in self.lights: lights.append(self._get_light_state(light)) if lights != self._last_lights: self._last_lights = lights return self._last_lights def set_all_off(self): for group in self.groups: group.on = False def get_all_off(self): for light in self.lights: if light.on: return False return True def set_alarm_lights_by_name(self, names): self._alarm_lights = [] if not isinstance(names, list): names = [names] for name in names: for light in self.lights: if light.name == name: self._alarm_lights.append(light) def alarm(self, active): if active: if not self._alarm_active: # Store current light state self._alarm_lights_state = [] for light in self._alarm_lights: self._alarm_lights_state.append( self._get_light_state(light)) for light in self._alarm_lights: if light.on: # Light is on, turn it off light.on = False else: # Light is off, change to red, and on light.on = True light.xy = [0.6708, 0.3205] light.brightness = 254 else: if self._alarm_active: # Alarm disabled, return to previous light state for itt, light in enumerate(self._alarm_lights): if self._alarm_lights_state[itt]['brightness'] > 0: # The light was originally on light.on = True light.brightness = self._alarm_lights_state[itt][ 'brightness'] light.xy = self._alarm_lights_state[itt]['xy'] else: # The light was originally on light.on = False self._alarm_active = active def _get_light_state(self, light): if light.on: return { 'id': light.light_id, 'brightness': light.brightness, 'xy': light.xy } else: return {'id': light.light_id, 'brightness': 0, 'xy': ''} def _get_scene_id_by_name(self, name): for scene in self.scenes: if scene.name == name: return scene.scene_id def _get_group_id_by_name(self, name): for group in self.groups: if group.name == name: return group.group_id def _compare_xy(self, xy_scene, xy_lights, xy_tolerance): if (xy_lights[0] > (xy_scene[0] - xy_tolerance)) and (xy_lights[0] < (xy_scene[0] + xy_tolerance)): if (xy_lights[1] > (xy_scene[1] - xy_tolerance)) and ( xy_lights[1] < (xy_scene[1] + xy_tolerance)): return True return False
class PhillipsHueSkill(MycroftSkill): def __init__(self): super(PhillipsHueSkill, self).__init__(name="PhillipsHueSkill") self.brightness_step = int( self.settings.get('brightness_step', DEFAULT_BRIGHTNESS_STEP)) self.color_temperature_step = \ int(self.settings.get('color_temperature_step', DEFAULT_COLOR_TEMPERATURE_STEP)) verbose = self.settings.get('verbose', False) if type(verbose) == str: verbose = verbose.lower() verbose = True if verbose == 'true' else False self.verbose = verbose self.username = self.settings.get('username') if self.username == '': self.username = None self.ip = None # set in _connect_to_bridge self.bridge = None self.default_group = None self.groups_to_ids_map = dict() self.scenes_to_ids_map = defaultdict(dict) self.colors_to_cie_color_map = self._map_colors_to_cie_colors( os.path.join(os.path.dirname(os.path.realpath(__file__)), "colors")) @property def connected(self): return self.bridge is not None @property def user_supplied_ip(self): return self.settings.get('ip') != '' @property def user_supplied_username(self): return self.settings.get('username') != '' def _register_with_bridge(self): """ Helper for connecting to the bridge. If we don't have a valid username for the bridge (ip) we are trying to use, this will cause one to be generated. """ self.speak_dialog('connect.to.bridge') i = 0 while i < 30: sleep(1) try: self.bridge = Bridge(self.ip) except PhueRegistrationException: continue else: break if not self.connected: self.speak_dialog('failed.to.register') else: self.speak_dialog('successfully.registered') def _update_bridge_data(self): """ This should be called any time a successful connection is established. It sets some member variables, and ensures that scenes and groups are registered as vocab. """ self.username = self.bridge.username with self.file_system.open('username', 'w') as conf_file: conf_file.write(self.username) if not self.default_group: self._set_default_group(self.settings.get('default_group')) self._register_groups_and_scenes() self._register_colors() def _attempt_connection(self): """ This will attempt to connect to the bridge, but will not handle any errors on it's own. Raises ------ UnauthorizedUserException If self.username is not None, and is not registered with the bridge """ if self.user_supplied_ip: self.ip = self.settings.get('ip') else: self.ip = _discover_bridge() if self.username: url = 'http://{ip}/api/{user}'.format(ip=self.ip, user=self.username) data = get(url).json() data = data[0] if isinstance(data, list) else data error = data.get('error') if error: description = error.get('description') if description == "unauthorized user": raise UnauthorizedUserException(self.username) else: raise Exception('Unknown Error: {0}'.format(description)) self.bridge = Bridge(self.ip, self.username) def _connect_to_bridge(self, acknowledge_successful_connection=False): """ Calls _attempt_connection, handling various exceptions by either alerting the user to issues with the config/setup, or registering the application with the bridge. Parameters ---------- acknowledge_successful_connection : bool Speak when a successful connection is established. Returns ------- bool True if a connection is established. """ try: self._attempt_connection() except DeviceNotFoundException: self.speak_dialog('bridge.not.found') return False except ConnectionError: self.speak_dialog('failed.to.connect') if self.user_supplied_ip: self.speak_dialog('ip.in.config') return False except socket.error as e: if 'No route to host' in e.args: self.speak_dialog('no.route') else: self.speak_dialog('failed.to.connect') return False except UnauthorizedUserException: if self.user_supplied_username: self.speak_dialog('invalid.user') return False else: self._register_with_bridge() except PhueRegistrationException: self._register_with_bridge() if not self.connected: return False if acknowledge_successful_connection: self.speak_dialog('successfully.connected') self._update_bridge_data() return True def _set_default_group(self, identifier): """ Sets the group to which commands will be applied, when a group is not specified in the command. Parameters ---------- identifier : str or int The name of the group, or it's integer id Notes ----- If the group does not exist, 0 (all lights) will be used. """ try: self.default_group = Group(self.bridge, identifier) except LookupError: self.speak_dialog('could.not.find.group', {'name': identifier}) self.speak_dialog('using.group.0') self.default_group = Group(self.bridge, 0) def _register_groups_and_scenes(self): """ Register group and scene names as vocab, and update our caches. """ groups = self.bridge.get_group() for id, group in groups.items(): name = group['name'].lower() self.groups_to_ids_map[name] = id self.register_vocabulary(name, "Group") scenes = self.bridge.get_scene() for id, scene in scenes.items(): name = scene['name'].lower() group_id = scene.get('group') group_id = int(group_id) if group_id else None self.scenes_to_ids_map[group_id][name] = id self.register_vocabulary(name, "Scene") def initialize(self): """ Attempt to connect to the bridge, and build/register intents. """ self.load_data_files(dirname(__file__)) if self.file_system.exists('username'): if not self.user_supplied_username: with self.file_system.open('username', 'r') as conf_file: self.username = conf_file.read().strip(' \n') try: self._attempt_connection() self._update_bridge_data() except (PhueRegistrationException, DeviceNotFoundException, UnauthorizedUserException, ConnectionError, socket.error): # Swallow it for now; _connect_to_bridge will deal with it pass toggle_intent = IntentBuilder("ToggleIntent") \ .one_of("OffKeyword", "OnKeyword") \ .one_of("Group", "LightsKeyword") \ .build() self.register_intent(toggle_intent, self.handle_toggle_intent) activate_scene_intent = IntentBuilder("ActivateSceneIntent") \ .require("Scene") \ .one_of("Group", "LightsKeyword") \ .build() self.register_intent(activate_scene_intent, self.handle_activate_scene_intent) adjust_brightness_intent = IntentBuilder("AdjustBrightnessIntent") \ .one_of("IncreaseKeyword", "DecreaseKeyword", "DimKeyword") \ .one_of("Group", "LightsKeyword") \ .optionally("BrightnessKeyword") \ .build() self.register_intent(adjust_brightness_intent, self.handle_adjust_brightness_intent) set_brightness_intent = IntentBuilder("SetBrightnessIntent") \ .require("Value") \ .one_of("Group", "LightsKeyword") \ .optionally("BrightnessKeyword") \ .build() self.register_intent(set_brightness_intent, self.handle_set_brightness_intent) adjust_color_temperature_intent = \ IntentBuilder("AdjustColorTemperatureIntent") \ .one_of("IncreaseKeyword", "DecreaseKeyword") \ .one_of("Group", "LightsKeyword") \ .require("ColorTemperatureKeyword") \ .build() self.register_intent(adjust_color_temperature_intent, self.handle_adjust_color_temperature_intent) connect_lights_intent = \ IntentBuilder("ConnectLightsIntent") \ .require("ConnectKeyword") \ .one_of("Group", "LightsKeyword") \ .build() self.register_intent(connect_lights_intent, self.handle_connect_lights_intent) change_color_intent = \ IntentBuilder("ChangeLightColorIntent") \ .require("Color") \ .one_of("Group", "LightsKeyword") \ .build() self.register_intent(change_color_intent, self.handle_change_color_intent) @intent_handler def handle_toggle_intent(self, message, group): if "OffKeyword" in message.data: dialog = 'turn.off' group.on = False else: dialog = 'turn.on' group.on = True if self.verbose: self.speak_dialog(dialog) @intent_handler def handle_activate_scene_intent(self, message, group): scene_name = message.data['Scene'].lower() scene_id = self.scenes_to_ids_map[group.group_id].get(scene_name) if not scene_id: scene_id = self.scenes_to_ids_map[None].get(scene_name) if scene_id: if self.verbose: self.speak_dialog('activate.scene', {'scene': scene_name}) self.bridge.activate_scene(group.group_id, scene_id) else: self.speak_dialog('scene.not.found', {'scene': scene_name}) @intent_handler def handle_adjust_brightness_intent(self, message, group): if "IncreaseKeyword" in message.data: brightness = group.brightness + self.brightness_step group.brightness = \ brightness if brightness < 255 else 254 dialog = 'increase.brightness' else: brightness = group.brightness - self.brightness_step group.brightness = brightness if brightness > 0 else 0 dialog = 'decrease.brightness' if self.verbose: self.speak_dialog(dialog) @intent_handler def handle_set_brightness_intent(self, message, group): value = int(message.data['Value'].rstrip('%')) brightness = int(value / 100.0 * 254) group.on = True group.brightness = brightness if self.verbose: self.speak_dialog('set.brightness', {'brightness': value}) @intent_handler def handle_adjust_color_temperature_intent(self, message, group): if "IncreaseKeyword" in message.data: color_temperature = \ group.colortemp_k + self.color_temperature_step group.colortemp_k = \ color_temperature if color_temperature < 6500 else 6500 dialog = 'increase.color.temperature' else: color_temperature = \ group.colortemp_k - self.color_temperature_step group.colortemp_k = \ color_temperature if color_temperature > 2000 else 2000 dialog = 'decrease.color.temperature' if self.verbose: self.speak_dialog(dialog) @intent_handler def handle_connect_lights_intent(self, message, group): if self.user_supplied_ip: self.speak_dialog('ip.in.config') return if self.verbose: self.speak_dialog('connecting') self._connect_to_bridge(acknowledge_successful_connection=True) @intent_handler def handle_change_color_intent(self, message, group): if message.data["Color"] in self.colors_to_cie_color_map: group.xy = self.colors_to_cie_color_map[message.data["Color"]] dialog = "change.color" else: dialog = "color.not.found" if self.verbose: self.speak_dialog(dialog, {"color": message.data["Color"]}) def stop(self): pass def _map_colors_to_cie_colors(self, color_files_directory_path): cie_colors_map = dict() try: colors_json = json.load( open( os.path.join(color_files_directory_path, "color-definitions.json"))) if self.lang.startswith("en"): for color_name, rgb_values in colors_json.items(): cie_colors_map[color_name] = self._rgb_to_cie( rgb_values[0], rgb_values[1], rgb_values[2]) else: color_mapping = json.load( open( self._get_color_file_path(color_files_directory_path))) for foreign_color_name, english_color_name in color_mapping.items( ): if english_color_name in colors_json: rgb_values = colors_json[english_color_name] cie_colors_map[foreign_color_name] = self._rgb_to_cie( rgb_values[0], rgb_values[1], rgb_values[2]) except Exception as e: LOGGER.error(e) return cie_colors_map def _get_color_file_path(self, color_files_directory_path): file_path = os.path.join(color_files_directory_path, self.lang.split("-")[0] + "-colors.json") return file_path if os.path.isfile(file_path) else "" def _register_colors(self): for color_name in self.colors_to_cie_color_map: self.register_vocabulary(color_name, "Color") """ This function is based on the project https://github.com/usolved/cie-rgb-converter which is licensed under MIT license. MIT License Copyright (c) 2017 www.usolved.net Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ def _rgb_to_cie(self, red, green, blue): red = pow((red + 0.055) / (1.0 + 0.055), 2.4) if (red > 0.04045) else (red / 12.92) green = pow( (green + 0.055) / (1.0 + 0.055), 2.4) if (green > 0.04045) else (green / 12.92) blue = pow((blue + 0.055) / (1.0 + 0.055), 2.4) if (blue > 0.04045) else (blue / 12.92) X = red * 0.664511 + green * 0.154324 + blue * 0.162028 Y = red * 0.283881 + green * 0.668433 + blue * 0.047685 Z = red * 0.000088 + green * 0.072310 + blue * 0.986039 x = round((X / (X + Y + Z)), 4) y = round((Y / (X + Y + Z)), 4) return [x, y]
class Component(ThreadComponent): MATRIX = matrices.LIGHT_BULB def __init__(self, component_config): super().__init__(component_config) self.bridge = Bridge(component_config['ip_address'], component_config['username']) self.id = component_config['id'] self.group_num = None self.scenes = {} self.first = component_config['first'] # TODO: Created groups are not deleted when a Nuimo is removed self.nuimo_mac_address = component_config['nuimo_mac_address'] global hue_instances global mac_idx if self.first and hue_instances != {} and self.nuimo_mac_address in hue_instances: temp = hue_instances[self.nuimo_mac_address]['mac_idx'] hue_instances[self.nuimo_mac_address] = {} hue_instances[self.nuimo_mac_address]['mac_idx'] = temp if self.nuimo_mac_address not in hue_instances: hue_instances[self.nuimo_mac_address] = {} hue_instances[self.nuimo_mac_address]['mac_idx'] = mac_idx mac_idx = mac_idx + 1 if self.id not in hue_instances[self.nuimo_mac_address]: hue_instances[self.nuimo_mac_address][self.id] = hue_instances[self.nuimo_mac_address]['mac_idx'] * 10 + len(hue_instances[self.nuimo_mac_address]) self.group_num = hue_instances[self.nuimo_mac_address][self.id] self.delta_range = range(-254, 254) self.delta = 0 # Extract light IDs, they are stored with format `<bridgeID>-light-<lightID>` light_ids = component_config['device_ids'] light_ids = [i.split('-light-')[1].strip() for i in light_ids] self.lights = self.create_lights(light_ids) self.lights.update_state() self.station_id_1 = component_config.get('station1', None) self.station_id_2 = component_config.get('station2', None) self.station_id_3 = component_config.get('station3', None) if not any((self.station_id_1, self.station_id_2, self.station_id_3)): try: self.scenes = self.bridge.get_scene() except ConnectionResetError: logger.error("Hue Bridge not reachable, handle exception") except socket.error as socketerror: logger.error("Socket Error: ", socketerror) self.scenes = {k: v for k, v in self.scenes.items() if v['lights'] == light_ids} if len(list(self.scenes.keys())) >= 3: for scene in self.scenes: self.station_id_1 = {'id': scene, 'name': self.scenes[scene]['name']} if self.scenes[scene]['name'] == 'Nightlight' else self.station_id_1 self.station_id_2 = {'id': scene, 'name': self.scenes[scene]['name']} if self.scenes[scene]['name'] == 'Relax' else self.station_id_2 self.station_id_3 = {'id': scene, 'name': self.scenes[scene]['name']} if self.scenes[scene]['name'] == 'Concentrate' else self.station_id_3 rands = sample(range(0, len(list(self.scenes.keys()))), 3) self.station_id_1 = {'id': list(self.scenes.keys())[rands[0]], 'name': self.scenes[list(self.scenes.keys())[rands[0]]]['name']} if self.station_id_1 is None else self.station_id_1 self.station_id_2 = {'id': list(self.scenes.keys())[rands[1]], 'name': self.scenes[list(self.scenes.keys())[rands[1]]]['name']} if self.station_id_2 is None else self.station_id_2 self.station_id_3 = {'id': list(self.scenes.keys())[rands[2]], 'name': self.scenes[list(self.scenes.keys())[rands[2]]]['name']} if self.station_id_3 is None else self.station_id_3 # seed random nr generator (used to get random color value) seed() def create_lights(self, light_ids): reachable_lights = None try: reachable_lights = self.filter_reachable(light_ids) except ConnectionResetError: # TODO: add a library wrapper to handle the issue properly, this is a workaround logger.error("Hue Bridge not reachable, handle exception") except socket.error as socketerror: logger.error("Socket Error: ", socketerror) if not reachable_lights: lights = EmptyLightSet() elif len(reachable_lights) > 10: lights = Group(self.bridge, reachable_lights, self.group_num, self.first) else: lights = LightSet(self.bridge, reachable_lights, self.group_num, self.first) return lights def filter_reachable(self, light_ids): lights = self.bridge.get_light() reachable = [i for i in light_ids if i in lights and lights[i]['state']['reachable']] logger.debug("lights: %s reachable: %s", list(lights.keys()), reachable) return reachable def on_button_press(self): self.set_light_attributes(on=not self.lights.on, bri=self.lights.brightness) def on_longtouch_left(self): logger.debug("on_longtouch_left()") if self.station_id_1 is not None: self.bridge.activate_scene('0', self.station_id_1['id']) self.nuimo.display_matrix(matrices.STATION1) def on_longtouch_bottom(self): logger.debug("on_longtouch_bottom()") if self.station_id_2 is not None: self.bridge.activate_scene('0', self.station_id_2['id']) self.nuimo.display_matrix(matrices.STATION2) def on_longtouch_right(self): logger.debug("on_longtouch_right()") if self.station_id_3 is not None: self.bridge.activate_scene('0', self.station_id_3['id']) self.nuimo.display_matrix(matrices.STATION3) def set_light_attributes(self, **attributes): response = self.lights.set_attributes(attributes) if 'errors' in response: logger.error("Failed to set light attributes: %s", response['errors']) self.nuimo.display_matrix(matrices.ERROR) return if 'xy' in attributes: if 'bri' in attributes: self.nuimo.display_matrix(matrices.LETTER_W) else: self.nuimo.display_matrix(matrices.SHUFFLE) elif 'on' in attributes and not ('bri_inc' in attributes): if self.lights.on: self.nuimo.display_matrix(matrices.LIGHT_ON) else: self.nuimo.display_matrix(matrices.LIGHT_OFF) elif 'on' in attributes or 'bri_inc' in attributes: if self.lights.brightness: matrix = matrices.progress_bar(self.lights.brightness / self.delta_range.stop) self.nuimo.display_matrix(matrix, fading=True, ignore_duplicates=True) else: self.set_light_attributes(on=False) def on_swipe_left(self): self.set_light_attributes(on=True, bri=self.lights.brightness, xy=COLOR_WHITE_XY) def on_swipe_right(self): self.set_light_attributes(on=True, xy=(random(), random())) def on_rotation(self, value): self.delta += value def run(self): prev_sync_time = time() prev_update_time = time() while not self.stopped: now = time() if self.delta and now - prev_update_time >= self.lights.update_interval: self.send_updates() self.delta = 0 prev_update_time = now if now - max([prev_sync_time, prev_update_time]) >= self.lights.sync_interval: try: self.lights.update_state() except ConnectionResetError: # TODO: add a library wrapper to handle the issue properly, this is a workaround logger.error("connection with Hue Bridge reset by peer, handle exception") except socket.error as socketerror: logger.error("Socket Error: ", socketerror) prev_sync_time = now sleep(0.05) def send_updates(self): delta = round(clamp_value(self.delta_range.stop * self.delta, self.delta_range)) if self.lights.on: self.set_light_attributes(bri_inc=delta) else: if delta > 0: self.set_light_attributes(on=True) self.set_light_attributes(bri_inc=delta)
focus_scene = 'eeuikaWaWMUVdw-' chill_scene = 'ExG65m1qcHIW6k-' dim_scene = 'j1uBBjWRkozFJDa' bridge_ip = '192.168.1.4' room_name = 'Gamekamer' ledstrip_id = 6 bulb_id = 3 # Insufficient arguments if len(sys.argv) < 2: sys.exit(1) # Initialise bridge bridge = Bridge(bridge_ip) # Connect if not yet connected if not os.path.isfile('~/.python_hue'): bridge.connect() # TODO: Refactor this mess if sys.argv[1] == 'focus': bridge.activate_scene(room_name, focus_scene) elif sys.argv[1] == 'chill': bridge.activate_scene(room_name, chill_scene) elif sys.argv[1] == 'dim': bridge.activate_scene(room_name, dim_scene) elif sys.argv[1] == 'notification': bridge.set_light(ledstrip_id, 'alert', 'select') elif sys.argv[1] == 'off': bridge.set_light([ledstrip_id, bulb_id], 'on', False)