Ejemplo n.º 1
0
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
Ejemplo n.º 2
0
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)
Ejemplo n.º 3
0
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"
Ejemplo n.º 4
0
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
Ejemplo n.º 5
0
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)
Ejemplo n.º 6
0
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
Ejemplo n.º 7
0
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]
Ejemplo n.º 8
0
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)
Ejemplo n.º 9
0
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)