Пример #1
0
class Switch(Device):
    """Class for managing a switch."""
    def __init__(self,
                 xknx,
                 name,
                 group_address=None,
                 group_address_state=None,
                 device_updated_cb=None):
        """Initialize Switch class."""
        # pylint: disable=too-many-arguments
        super().__init__(xknx, name, device_updated_cb)

        self.switch = RemoteValueSwitch(xknx,
                                        group_address,
                                        group_address_state,
                                        device_name=self.name,
                                        after_update_cb=self.after_update)

    def _iter_remote_values(self):
        """Iterate the devices RemoteValue classes."""
        yield self.switch

    @classmethod
    def from_config(cls, xknx, name, config):
        """Initialize object from configuration structure."""
        group_address = \
            config.get('group_address')
        group_address_state = \
            config.get('group_address_state')

        return cls(xknx,
                   name,
                   group_address=group_address,
                   group_address_state=group_address_state)

    @property
    def state(self):
        """Return the current switch state of the device."""
        # None will return False
        return bool(self.switch.value)

    async def set_on(self):
        """Switch on switch."""
        await self.switch.on()

    async def set_off(self):
        """Switch off switch."""
        await self.switch.off()

    async def do(self, action):
        """Execute 'do' commands."""
        if action == "on":
            await self.set_on()
        elif action == "off":
            await self.set_off()
        else:
            self.xknx.logger.warning(
                "Could not understand action %s for device %s", action,
                self.get_name())

    async def process_group_write(self, telegram):
        """Process incoming GROUP WRITE telegram."""
        await self.switch.process(telegram)

    def __str__(self):
        """Return object as readable string."""
        return '<Switch name="{0}" switch="{1}" />' \
            .format(self.name,
                    self.switch.group_addr_str())

    def __eq__(self, other):
        """Equal operator."""
        return self.__dict__ == other.__dict__
Пример #2
0
class Light(Device):
    """Class for managing a light."""

    DEFAULT_MIN_KELVIN = 2700  # 370 mireds
    DEFAULT_MAX_KELVIN = 6000  # 166 mireds

    def __init__(
        self,
        xknx: XKNX,
        name: str,
        group_address_switch: GroupAddressesType | None = None,
        group_address_switch_state: GroupAddressesType | None = None,
        group_address_brightness: GroupAddressesType | None = None,
        group_address_brightness_state: GroupAddressesType | None = None,
        group_address_color: GroupAddressesType | None = None,
        group_address_color_state: GroupAddressesType | None = None,
        group_address_rgbw: GroupAddressesType | None = None,
        group_address_rgbw_state: GroupAddressesType | None = None,
        group_address_tunable_white: GroupAddressesType | None = None,
        group_address_tunable_white_state: GroupAddressesType | None = None,
        group_address_color_temperature: GroupAddressesType | None = None,
        group_address_color_temperature_state: GroupAddressesType
        | None = None,
        group_address_switch_red: GroupAddressesType | None = None,
        group_address_switch_red_state: GroupAddressesType | None = None,
        group_address_brightness_red: GroupAddressesType | None = None,
        group_address_brightness_red_state: GroupAddressesType | None = None,
        group_address_switch_green: GroupAddressesType | None = None,
        group_address_switch_green_state: GroupAddressesType | None = None,
        group_address_brightness_green: GroupAddressesType | None = None,
        group_address_brightness_green_state: GroupAddressesType | None = None,
        group_address_switch_blue: GroupAddressesType | None = None,
        group_address_switch_blue_state: GroupAddressesType | None = None,
        group_address_brightness_blue: GroupAddressesType | None = None,
        group_address_brightness_blue_state: GroupAddressesType | None = None,
        group_address_switch_white: GroupAddressesType | None = None,
        group_address_switch_white_state: GroupAddressesType | None = None,
        group_address_brightness_white: GroupAddressesType | None = None,
        group_address_brightness_white_state: GroupAddressesType | None = None,
        min_kelvin: int | None = None,
        max_kelvin: int | None = None,
        device_updated_cb: DeviceCallbackType | None = None,
    ):
        """Initialize Light class."""
        super().__init__(xknx, name, device_updated_cb)

        self.switch = RemoteValueSwitch(
            xknx,
            group_address_switch,
            group_address_switch_state,
            device_name=self.name,
            feature_name="State",
            after_update_cb=self.after_update,
        )

        self.brightness = RemoteValueScaling(
            xknx,
            group_address_brightness,
            group_address_brightness_state,
            device_name=self.name,
            feature_name="Brightness",
            after_update_cb=self.after_update,
            range_from=0,
            range_to=255,
        )

        self.color = RemoteValueColorRGB(
            xknx,
            group_address_color,
            group_address_color_state,
            device_name=self.name,
            after_update_cb=self.after_update,
        )

        self.rgbw = RemoteValueColorRGBW(
            xknx,
            group_address_rgbw,
            group_address_rgbw_state,
            device_name=self.name,
            after_update_cb=self.after_update,
        )

        self.tunable_white = RemoteValueScaling(
            xknx,
            group_address_tunable_white,
            group_address_tunable_white_state,
            device_name=self.name,
            feature_name="Tunable white",
            after_update_cb=self.after_update,
            range_from=0,
            range_to=255,
        )

        self.color_temperature = RemoteValueDpt2ByteUnsigned(
            xknx,
            group_address_color_temperature,
            group_address_color_temperature_state,
            device_name=self.name,
            feature_name="Color temperature",
            after_update_cb=self.after_update,
        )

        self.red = _SwitchAndBrightness(
            xknx,
            self.name,
            "red",
            group_address_switch_red,
            group_address_switch_red_state,
            group_address_brightness_red,
            group_address_brightness_red_state,
            self.after_update,
        )

        self.green = _SwitchAndBrightness(
            xknx,
            self.name,
            "green",
            group_address_switch_green,
            group_address_switch_green_state,
            group_address_brightness_green,
            group_address_brightness_green_state,
            self.after_update,
        )

        self.blue = _SwitchAndBrightness(
            xknx,
            self.name,
            "blue",
            group_address_switch_blue,
            group_address_switch_blue_state,
            group_address_brightness_blue,
            group_address_brightness_blue_state,
            self.after_update,
        )

        self.white = _SwitchAndBrightness(
            xknx,
            self.name,
            "white",
            group_address_switch_white,
            group_address_switch_white_state,
            group_address_brightness_white,
            group_address_brightness_white_state,
            self.after_update,
        )

        self.min_kelvin = min_kelvin
        self.max_kelvin = max_kelvin

    def _iter_remote_values(self) -> Iterator[RemoteValue[Any, Any]]:
        """Iterate the devices RemoteValue classes."""
        yield self.switch
        yield self.brightness
        yield self.color
        yield self.rgbw
        yield self.tunable_white
        yield self.color_temperature
        for color in self._iter_individual_colors():
            yield color.switch
            yield color.brightness

    def _iter_individual_colors(self) -> Iterator[_SwitchAndBrightness]:
        """Iterate the devices individual colors."""
        yield from (self.red, self.green, self.blue, self.white)

    @property
    def unique_id(self) -> str | None:
        """Return unique id for this device."""
        if self.switch.group_address is not None:
            return f"{self.switch.group_address}"

        return (
            f"{self.red.brightness.group_address}_{self.green.brightness.group_address}_"
            f"{self.blue.brightness.group_address}_{self.white.brightness.group_address}"
        )

    @property
    def supports_brightness(self) -> bool:
        """Return if light supports brightness."""
        return self.brightness.initialized

    @property
    def supports_color(self) -> bool:
        """Return if light supports color."""
        return self.color.initialized or all(
            c.brightness.initialized
            for c in (self.red, self.green, self.blue))

    @property
    def supports_rgbw(self) -> bool:
        """Return if light supports RGBW."""
        return self.rgbw.initialized or all(
            c.brightness.initialized for c in self._iter_individual_colors())

    @property
    def supports_tunable_white(self) -> bool:
        """Return if light supports tunable white / relative color temperature."""
        return self.tunable_white.initialized

    @property
    def supports_color_temperature(self) -> bool:
        """Return if light supports absolute color temperature."""
        return self.color_temperature.initialized

    def __str__(self) -> str:
        """Return object as readable string."""
        str_brightness = ("" if not self.supports_brightness else
                          f" brightness={self.brightness.group_addr_str()}")

        str_color = ("" if not self.supports_color else
                     f" color={self.color.group_addr_str()}")

        str_rgbw = ("" if not self.supports_rgbw else
                    f" rgbw={self.rgbw.group_addr_str()}")

        str_tunable_white = (
            "" if not self.supports_tunable_white else
            f" tunable_white={self.tunable_white.group_addr_str()}")

        str_color_temperature = (
            "" if not self.supports_color_temperature else
            f" color_temperature={self.color_temperature.group_addr_str()}")

        str_red_state = ("" if not self.red.switch.initialized else
                         f" red_state={self.red.switch.group_addr_str()}")
        str_red_brightness = (
            "" if not self.red.brightness.initialized else
            f" red_brightness={self.red.brightness.group_addr_str()}")

        str_green_state = (
            "" if not self.green.switch.initialized else
            f" green_state={self.green.switch.group_addr_str()}")
        str_green_brightness = (
            "" if not self.green.brightness.initialized else
            f" green_brightness={self.green.brightness.group_addr_str()}")

        str_blue_state = ("" if not self.blue.switch.initialized else
                          f" blue_state={self.blue.switch.group_addr_str()}")
        str_blue_brightness = (
            "" if not self.blue.brightness.initialized else
            f" blue_brightness={self.blue.brightness.group_addr_str()}")

        str_white_state = (
            "" if not self.white.switch.initialized else
            f" white_state={self.white.switch.group_addr_str()}")
        str_white_brightness = (
            "" if not self.white.brightness.initialized else
            f" white_brightness={self.white.brightness.group_addr_str()}")

        return '<Light name="{}" ' "switch={}{}{}{}{}{}{}{}{}{}{}{}{}{} />".format(
            self.name,
            self.switch.group_addr_str(),
            str_brightness,
            str_color,
            str_rgbw,
            str_tunable_white,
            str_color_temperature,
            str_red_state,
            str_red_brightness,
            str_green_state,
            str_green_brightness,
            str_blue_state,
            str_blue_brightness,
            str_white_state,
            str_white_brightness,
        )

    @property
    def state(self) -> bool | None:
        """Return the current switch state of the device."""
        if self.switch.value is not None:
            return self.switch.value
        if any(c.is_on is not None for c in self._iter_individual_colors()):
            return any(c.is_on for c in self._iter_individual_colors())
        return None

    async def set_on(self) -> None:
        """Switch light on."""
        if self.switch.initialized:
            await self.switch.on()
        for color in self._iter_individual_colors():
            await color.set_on()

    async def set_off(self) -> None:
        """Switch light off."""
        if self.switch.initialized:
            await self.switch.off()
        for color in self._iter_individual_colors():
            await color.set_off()

    @property
    def current_brightness(self) -> int | None:
        """Return current brightness of light between 0..255."""
        return self.brightness.value

    async def set_brightness(self, brightness: int) -> None:
        """Set brightness of light."""
        if not self.supports_brightness:
            logger.warning("Dimming not supported for device %s",
                           self.get_name())
            return
        await self.brightness.set(brightness)

    @property
    def current_color(self) -> tuple[tuple[int, int, int] | None, int | None]:
        """
        Return current color of light.

        If the device supports RGBW, get the current RGB+White values instead.
        """
        if self.supports_rgbw:
            if self.rgbw.initialized:
                if not self.rgbw.value:
                    return None, None
                return self.rgbw.value[:3], self.rgbw.value[3]
        if self.color.initialized:
            return self.color.value, None
        # individual RGB addresses - white will return None when it is not initialized
        colors = (
            self.red.brightness.value,
            self.green.brightness.value,
            self.blue.brightness.value,
        )
        if None in colors:
            return None, self.white.brightness.value
        return cast(Tuple[int, int, int], colors), self.white.brightness.value

    async def set_color(self,
                        color: tuple[int, int, int],
                        white: int | None = None) -> None:
        """
        Set color of a light device.

        If also the white value is given and the device supports RGBW,
        set all four values.
        """
        if white is not None:
            if self.supports_rgbw:
                if self.rgbw.initialized:
                    await self.rgbw.set(list(color) + [white])
                    return
                if all(c.brightness.initialized
                       for c in self._iter_individual_colors()):
                    await self.red.brightness.set(color[0])
                    await self.green.brightness.set(color[1])
                    await self.blue.brightness.set(color[2])
                    await self.white.brightness.set(white)
                    return
            logger.warning("RGBW not supported for device %s", self.get_name())
        else:
            if self.supports_color:
                if self.color.initialized:
                    await self.color.set(color)
                    return
                if all(c.brightness.initialized
                       for c in (self.red, self.green, self.blue)):
                    await self.red.brightness.set(color[0])
                    await self.green.brightness.set(color[1])
                    await self.blue.brightness.set(color[2])
                    return
            logger.warning("Colors not supported for device %s",
                           self.get_name())

    @property
    def current_tunable_white(self) -> int | None:
        """Return current relative color temperature of light."""
        return self.tunable_white.value

    async def set_tunable_white(self, tunable_white: int) -> None:
        """Set relative color temperature of light."""
        if not self.supports_tunable_white:
            logger.warning("Tunable white not supported for device %s",
                           self.get_name())
            return
        await self.tunable_white.set(tunable_white)

    @property
    def current_color_temperature(self) -> int | None:
        """Return current absolute color temperature of light."""
        return self.color_temperature.value

    async def set_color_temperature(self, color_temperature: int) -> None:
        """Set absolute color temperature of light."""
        if not self.supports_color_temperature:
            logger.warning(
                "Absolute Color Temperature not supported for device %s",
                self.get_name(),
            )
            return
        await self.color_temperature.set(color_temperature)

    async def process_group_write(self, telegram: "Telegram") -> None:
        """Process incoming and outgoing GROUP WRITE telegram."""
        for remote_value in self._iter_remote_values():
            await remote_value.process(telegram)
Пример #3
0
class BinarySensor(Device):
    """Class for binary sensor."""

    # pylint: disable=too-many-instance-attributes
    def __init__(
        self,
        xknx,
        name,
        group_address_state=None,
        sync_state: bool = True,
        ignore_internal_state: bool = True,
        device_class: str = None,
        reset_after: Optional[float] = None,
        actions: List[Action] = None,
        context_timeout: Optional[float] = None,
        device_updated_cb=None,
    ):
        """Initialize BinarySensor class."""
        # pylint: disable=too-many-arguments
        super().__init__(xknx, name, device_updated_cb)
        if actions is None:
            actions = []

        self.actions = actions
        self.device_class = device_class
        self.ignore_internal_state = ignore_internal_state
        self.reset_after = reset_after
        self.state = None

        self._context_timeout = context_timeout
        self._count_set_on = 0
        self._count_set_off = 0
        self._last_set = None
        self._reset_task = None
        self._context_task = None
        # TODO: log a warning if reset_after and sync_state are true ? This could cause actions to self-fire.
        self.remote_value = RemoteValueSwitch(
            xknx,
            group_address_state=group_address_state,
            sync_state=sync_state,
            device_name=self.name,
            # after_update called internally
            after_update_cb=self._state_from_remote_value,
        )

    def _iter_remote_values(self):
        """Iterate the devices RemoteValue classes."""
        yield self.remote_value

    def __del__(self):
        """Destructor. Cleaning up if this was not done before."""
        if self._reset_task:
            self._reset_task.cancel()

        if self._context_task:
            self._context_task.cancel()

    @classmethod
    def from_config(cls, xknx, name, config):
        """Initialize object from configuration structure."""
        group_address_state = config.get("group_address_state")
        context_timeout = config.get("context_timeout")
        reset_after = config.get("reset_after")
        sync_state = config.get("sync_state", True)
        device_class = config.get("device_class")
        ignore_internal_state = config.get("ignore_internal_state", True)
        actions = []
        if "actions" in config:
            for action in config["actions"]:
                action = Action.from_config(xknx, action)
                actions.append(action)

        return cls(
            xknx,
            name,
            group_address_state=group_address_state,
            sync_state=sync_state,
            ignore_internal_state=ignore_internal_state,
            context_timeout=context_timeout,
            reset_after=reset_after,
            device_class=device_class,
            actions=actions,
        )

    async def _state_from_remote_value(self):
        """Update the internal state from RemoteValue (Callback)."""
        await self._set_internal_state(self.remote_value.value)

    async def _set_internal_state(self, state: bool):
        """Set the internal state of the device. If state was changed after_update hooks and connected Actions are executed."""
        if state != self.state or self.ignore_internal_state:
            self.state = state
            self.bump_and_get_counter(state)

            if self.ignore_internal_state and self._context_timeout:
                if self._context_task:
                    self._context_task.cancel()
                self._context_task = asyncio.create_task(
                    self._counter_task(self._context_timeout))
            else:
                await self._trigger_callbacks()

    async def _counter_task(self, wait_seconds: float):
        """Trigger after 1 second to prevent double triggers."""
        await asyncio.sleep(wait_seconds)
        await self._trigger_callbacks()

        self._count_set_on = 0
        self._count_set_off = 0

        await self.after_update()

    async def _trigger_callbacks(self):
        """Trigger callbacks for device and execute actions if any."""
        await self.after_update()

        for action in self.actions:
            if action.test_if_applicable(self.state, self.counter):
                await action.execute()

    @property
    def counter(self):
        """Return current counter for sensor."""
        return self._count_set_on if self.state else self._count_set_off

    def bump_and_get_counter(self, state: bool) -> int:
        """Bump counter and return the number of times a state was set to the same value within CONTEXT_TIMEOUT."""
        def within_same_context() -> bool:
            """Check if state change was within same context (e.g. 'Button was pressed twice')."""
            if self._last_set is None:
                self._last_set = time.time()
                return False
            new_set_time = time.time()
            time_diff = new_set_time - self._last_set
            self._last_set = new_set_time
            return time_diff < self._context_timeout

        if self._context_timeout and within_same_context():
            if state:
                self._count_set_on = self._count_set_on + 1
                return self._count_set_on
            self._count_set_off = self._count_set_off + 1
            return self._count_set_off

        if state:
            self._count_set_on = 1
            self._count_set_off = 0
        else:
            self._count_set_on = 0
            self._count_set_off = 1
        return 1

    async def process_group_write(self, telegram):
        """Process incoming and outgoing GROUP WRITE telegram."""
        if await self.remote_value.process(telegram, always_callback=True):

            if self.reset_after is not None and self.state:
                if self._reset_task:
                    self._reset_task.cancel()
                self._reset_task = asyncio.create_task(
                    self._reset_state(self.reset_after))

    async def _reset_state(self, wait_seconds: float):
        await asyncio.sleep(wait_seconds)
        await self._set_internal_state(False)

    def is_on(self) -> bool:
        """Return if binary sensor is 'on'."""
        return bool(self.state)

    def is_off(self) -> bool:
        """Return if binary sensor is 'off'."""
        return not self.state

    def __str__(self):
        """Return object as readable string."""
        return '<BinarySensor name="{}" remote_value="{}" state="{}"/>'.format(
            self.name, self.remote_value.group_addr_str(), self.state)
Пример #4
0
class Cover(Device):
    """Class for managing a cover."""

    # pylint: disable=too-many-instance-attributes
    # pylint: disable=too-many-public-methods
    # pylint: disable=too-many-locals

    # Average typical travel time of a cover
    DEFAULT_TRAVEL_TIME_DOWN = 22
    DEFAULT_TRAVEL_TIME_UP = 22

    def __init__(
        self,
        xknx: "XKNX",
        name: str,
        group_address_long: Optional["GroupAddressableType"] = None,
        group_address_short: Optional["GroupAddressableType"] = None,
        group_address_stop: Optional["GroupAddressableType"] = None,
        group_address_position: Optional["GroupAddressableType"] = None,
        group_address_position_state: Optional["GroupAddressableType"] = None,
        group_address_angle: Optional["GroupAddressableType"] = None,
        group_address_angle_state: Optional["GroupAddressableType"] = None,
        travel_time_down: float = DEFAULT_TRAVEL_TIME_DOWN,
        travel_time_up: float = DEFAULT_TRAVEL_TIME_UP,
        invert_position: bool = False,
        invert_angle: bool = False,
        device_updated_cb: Optional[DeviceCallbackType] = None,
        device_class: Optional[str] = None,
    ):
        """Initialize Cover class."""
        # pylint: disable=too-many-arguments
        super().__init__(xknx, name, device_updated_cb)
        # self.after_update for position changes is called after updating the
        # travelcalculator (in process_group_write and set_*) - angle changes
        # are updated from RemoteValue objects
        self.updown = RemoteValueUpDown(
            xknx,
            group_address_long,
            device_name=self.name,
            after_update_cb=None,
            invert=invert_position,
        )

        self.step = RemoteValueStep(
            xknx,
            group_address_short,
            device_name=self.name,
            after_update_cb=self.after_update,
            invert=invert_position,
        )

        self.stop_ = RemoteValueSwitch(
            xknx,
            group_address=group_address_stop,
            device_name=self.name,
            after_update_cb=None,
        )

        position_range_from = 100 if invert_position else 0
        position_range_to = 0 if invert_position else 100
        self.position_current = RemoteValueScaling(
            xknx,
            group_address_state=group_address_position_state,
            device_name=self.name,
            feature_name="Position",
            after_update_cb=self._current_position_from_rv,
            range_from=position_range_from,
            range_to=position_range_to,
        )
        self.position_target = RemoteValueScaling(
            xknx,
            group_address=group_address_position,
            device_name=self.name,
            feature_name="Target position",
            after_update_cb=self._target_position_from_rv,
            range_from=position_range_from,
            range_to=position_range_to,
        )

        angle_range_from = 100 if invert_angle else 0
        angle_range_to = 0 if invert_angle else 100
        self.angle = RemoteValueScaling(
            xknx,
            group_address_angle,
            group_address_angle_state,
            device_name=self.name,
            feature_name="Tilt angle",
            after_update_cb=self.after_update,
            range_from=angle_range_from,
            range_to=angle_range_to,
        )

        self.travel_time_down = travel_time_down
        self.travel_time_up = travel_time_up

        self.travelcalculator = TravelCalculator(travel_time_down, travel_time_up)

        self.device_class = device_class

    def _iter_remote_values(self) -> Iterator["RemoteValue"]:
        """Iterate the devices RemoteValue classes."""
        yield from (
            self.updown,
            self.step,
            self.stop_,
            self.position_current,
            self.position_target,
            self.angle,
        )

    @classmethod
    def from_config(cls, xknx: "XKNX", name: str, config: Any) -> "Cover":
        """Initialize object from configuration structure."""
        group_address_long = config.get("group_address_long")
        group_address_short = config.get("group_address_short")
        group_address_stop = config.get("group_address_stop")
        group_address_position = config.get("group_address_position")
        group_address_position_state = config.get("group_address_position_state")
        group_address_angle = config.get("group_address_angle")
        group_address_angle_state = config.get("group_address_angle_state")
        travel_time_down = config.get("travel_time_down", cls.DEFAULT_TRAVEL_TIME_DOWN)
        travel_time_up = config.get("travel_time_up", cls.DEFAULT_TRAVEL_TIME_UP)
        invert_position = config.get("invert_position", False)
        invert_angle = config.get("invert_angle", False)
        device_class = config.get("device_class")

        return cls(
            xknx,
            name,
            group_address_long=group_address_long,
            group_address_short=group_address_short,
            group_address_stop=group_address_stop,
            group_address_position=group_address_position,
            group_address_position_state=group_address_position_state,
            group_address_angle=group_address_angle,
            group_address_angle_state=group_address_angle_state,
            travel_time_down=travel_time_down,
            travel_time_up=travel_time_up,
            invert_position=invert_position,
            invert_angle=invert_angle,
            device_class=device_class,
        )

    def __str__(self) -> str:
        """Return object as readable string."""
        return (
            '<Cover name="{}" '
            'updown="{}" '
            'step="{}" '
            'stop="{}" '
            'position_current="{}" '
            'position_target="{}" '
            'angle="{}" '
            'travel_time_down="{}" '
            'travel_time_up="{}" />'.format(
                self.name,
                self.updown.group_addr_str(),
                self.step.group_addr_str(),
                self.stop_.group_addr_str(),
                self.position_current.group_addr_str(),
                self.position_target.group_addr_str(),
                self.angle.group_addr_str(),
                self.travel_time_down,
                self.travel_time_up,
            )
        )

    async def set_down(self) -> None:
        """Move cover down."""
        await self.updown.down()
        self.travelcalculator.start_travel_down()
        await self.after_update()

    async def set_up(self) -> None:
        """Move cover up."""
        await self.updown.up()
        self.travelcalculator.start_travel_up()
        await self.after_update()

    async def set_short_down(self) -> None:
        """Move cover short down."""
        await self.step.increase()

    async def set_short_up(self) -> None:
        """Move cover short up."""
        await self.step.decrease()

    async def stop(self) -> None:
        """Stop cover."""
        if self.stop_.writable:
            await self.stop_.on()
        elif self.step.writable:
            await self.step.increase()
        else:
            logger.warning("Stop not supported for device %s", self.get_name())
            return
        self.travelcalculator.stop()
        await self.after_update()

    async def set_position(self, position: int) -> None:
        """Move cover to a desginated postion."""
        if not self.position_target.writable:
            # No direct positioning group address defined
            # fully open or close is always possible even if current position is not known
            current_position = self.current_position()
            if current_position is None:
                if position == self.travelcalculator.position_open:
                    await self.updown.up()
                elif position == self.travelcalculator.position_closed:
                    await self.updown.down()
                else:
                    logger.warning(
                        "Current position unknown. Initialize cover by moving to end position."
                    )
                    return
            elif position < current_position:
                await self.updown.up()
            elif position > current_position:
                await self.updown.down()
            self.travelcalculator.start_travel(position)
            await self.after_update()
        else:
            await self.position_target.set(position)

    async def _target_position_from_rv(self) -> None:
        """Update the target postion from RemoteValue (Callback)."""
        self.travelcalculator.start_travel(self.position_target.value)
        await self.after_update()

    async def _current_position_from_rv(self) -> None:
        """Update the current postion from RemoteValue (Callback)."""
        position_before_update = self.travelcalculator.current_position()
        if self.is_traveling():
            self.travelcalculator.update_position(self.position_current.value)
        else:
            self.travelcalculator.set_position(self.position_current.value)
        if position_before_update != self.travelcalculator.current_position():
            await self.after_update()

    async def set_angle(self, angle: int) -> None:
        """Move cover to designated angle."""
        if not self.supports_angle:
            logger.warning("Angle not supported for device %s", self.get_name())
            return
        await self.angle.set(angle)

    async def auto_stop_if_necessary(self) -> None:
        """Do auto stop if necessary."""
        # If device does not support auto_positioning,
        # we have to stop the device when position is reached.
        # unless device was traveling to fully open
        # or fully closed state
        if (
            self.supports_stop
            and not self.position_target.writable
            and self.position_reached()
            and not self.is_open()
            and not self.is_closed()
        ):
            await self.stop()

    async def do(self, action: str) -> None:
        """Execute 'do' commands."""
        if action == "up":
            await self.set_up()
        elif action == "short_up":
            await self.set_short_up()
        elif action == "down":
            await self.set_down()
        elif action == "short_down":
            await self.set_short_down()
        elif action == "stop":
            await self.stop()
        else:
            logger.warning(
                "Could not understand action %s for device %s", action, self.get_name()
            )

    async def sync(self, wait_for_result: bool = False) -> None:
        """Read states of device from KNX bus."""
        await self.position_current.read_state(wait_for_result=wait_for_result)
        await self.angle.read_state(wait_for_result=wait_for_result)

    async def process_group_write(self, telegram: "Telegram") -> None:
        """Process incoming and outgoing GROUP WRITE telegram."""
        # call after_update to account for travelcalculator changes
        if await self.updown.process(telegram):
            if (
                not self.is_opening()
                and self.updown.value == RemoteValueUpDown.Direction.UP
            ):
                self.travelcalculator.start_travel_up()
                await self.after_update()
            elif (
                not self.is_closing()
                and self.updown.value == RemoteValueUpDown.Direction.DOWN
            ):
                self.travelcalculator.start_travel_down()
                await self.after_update()
        # stop from bus
        if await self.stop_.process(telegram) or await self.step.process(telegram):
            if self.is_traveling():
                self.travelcalculator.stop()
                await self.after_update()

        await self.position_current.process(telegram, always_callback=True)
        await self.position_target.process(telegram)
        await self.angle.process(telegram)

    def current_position(self) -> Optional[int]:
        """Return current position of cover."""
        return self.travelcalculator.current_position()

    def current_angle(self) -> Optional[int]:
        """Return current tilt angle of cover."""
        return self.angle.value  # type: ignore

    def is_traveling(self) -> bool:
        """Return if cover is traveling at the moment."""
        return self.travelcalculator.is_traveling()

    def position_reached(self) -> bool:
        """Return if cover has reached its final position."""
        return self.travelcalculator.position_reached()

    def is_open(self) -> bool:
        """Return if cover is open."""
        return self.travelcalculator.is_open()

    def is_closed(self) -> bool:
        """Return if cover is closed."""
        return self.travelcalculator.is_closed()

    def is_opening(self) -> bool:
        """Return if the cover is opening or not."""
        return self.travelcalculator.is_opening()

    def is_closing(self) -> bool:
        """Return if the cover is closing or not."""
        return self.travelcalculator.is_closing()

    @property
    def supports_stop(self) -> bool:
        """Return if cover supports manual stopping."""
        return self.stop_.writable or self.step.writable

    @property
    def supports_position(self) -> bool:
        """Return if cover supports direct positioning."""
        return self.position_target.initialized

    @property
    def supports_angle(self) -> bool:
        """Return if cover supports tilt angle."""
        return self.angle.initialized
Пример #5
0
class BinarySensor(Device):
    """Class for binary sensor."""

    # pylint: disable=too-many-instance-attributes

    CONTEXT_TIMEOUT = 1

    def __init__(self,
                 xknx,
                 name,
                 group_address_state=None,
                 sync_state=True,
                 ignore_internal_state=False,
                 device_class=None,
                 reset_after=None,
                 actions=None,
                 device_updated_cb=None):
        """Initialize BinarySensor class."""
        # pylint: disable=too-many-arguments
        super().__init__(xknx, name, device_updated_cb)
        if actions is None:
            actions = []

        self.actions = actions
        self.device_class = device_class
        self.ignore_internal_state = ignore_internal_state
        self.reset_after = reset_after
        self.state = False

        self._count_set_on = 0
        self._count_set_off = 0
        self._last_set = None
        self._reset_task = None
        # TODO: log a warning if reset_after and sync_state are true ? This could cause actions to self-fire.
        self.remote_value = RemoteValueSwitch(
            xknx,
            group_address_state=group_address_state,
            sync_state=sync_state,
            device_name=self.name,
            # after_update called internally
            after_update_cb=self._state_from_remote_value)

    def _iter_remote_values(self):
        """Iterate the devices RemoteValue classes."""
        yield self.remote_value

    def __del__(self):
        """Destructor. Cleaning up if this was not done before."""
        if self._reset_task:
            self._reset_task.cancel()

    @classmethod
    def from_config(cls, xknx, name, config):
        """Initialize object from configuration structure."""
        group_address_state = \
            config.get('group_address_state')
        sync_state = \
            config.get('sync_state', True)
        device_class = \
            config.get('device_class')
        ignore_internal_state = \
            config.get('ignore_internal_state', False)
        actions = []
        if "actions" in config:
            for action in config["actions"]:
                action = Action.from_config(xknx, action)
                actions.append(action)

        return cls(xknx,
                   name,
                   group_address_state=group_address_state,
                   sync_state=sync_state,
                   ignore_internal_state=ignore_internal_state,
                   device_class=device_class,
                   actions=actions)

    async def _state_from_remote_value(self):
        """Update the internal state from ReomteValue (Callback)."""
        await self._set_internal_state(self.remote_value.value)

    async def _set_internal_state(self, state):
        """Set the internal state of the device. If state was changed after_update hooks and connected Actions are executed."""
        if state != self.state or self.ignore_internal_state:
            self.state = state
            counter = self.bump_and_get_counter(state)
            await self.after_update()

            for action in self.actions:
                if action.test_if_applicable(self.state, counter):
                    await action.execute()

    def bump_and_get_counter(self, state):
        """Bump counter and return the number of times a state was set to the same value within CONTEXT_TIMEOUT."""
        def within_same_context():
            """Check if state change was within same context (e.g. 'Button was pressed twice')."""
            if self._last_set is None:
                self._last_set = time.time()
                return False
            new_set_time = time.time()
            time_diff = new_set_time - self._last_set
            self._last_set = new_set_time
            return time_diff < self.CONTEXT_TIMEOUT

        if within_same_context():
            if state:
                self._count_set_on = self._count_set_on + 1
                return self._count_set_on
            self._count_set_off = self._count_set_off + 1
            return self._count_set_off

        if state:
            self._count_set_on = 1
            self._count_set_off = 0
        else:
            self._count_set_on = 0
            self._count_set_off = 1
        return 1

    async def process_group_write(self, telegram):
        """Process incoming GROUP WRITE telegram."""
        if await self.remote_value.process(telegram, always_callback=True):

            if self.reset_after is not None and \
                    self.state:
                if self._reset_task:
                    self._reset_task.cancel()
                self._reset_task = self.xknx.loop.create_task(
                    self._reset_state(self.reset_after / 1000))

    async def _reset_state(self, wait_seconds):
        await asyncio.sleep(wait_seconds)
        await self._set_internal_state(False)

    def is_on(self):
        """Return if binary sensor is 'on'."""
        return self.state

    def is_off(self):
        """Return if binary sensor is 'off'."""
        return not self.state

    def __str__(self):
        """Return object as readable string."""
        return '<BinarySensor name="{0}" remote_value="{1}" state="{2}"/>' \
            .format(self.name, self.remote_value.group_addr_str(), self.state)

    def __eq__(self, other):
        """Equal operator."""
        return self.__dict__ == other.__dict__
Пример #6
0
class Weather(Device):
    """Class for managing a weather device."""

    def __init__(
        self,
        xknx: XKNX,
        name: str,
        group_address_temperature: GroupAddressesType | None = None,
        group_address_brightness_south: GroupAddressesType | None = None,
        group_address_brightness_north: GroupAddressesType | None = None,
        group_address_brightness_west: GroupAddressesType | None = None,
        group_address_brightness_east: GroupAddressesType | None = None,
        group_address_wind_speed: GroupAddressesType | None = None,
        group_address_wind_bearing: GroupAddressesType | None = None,
        group_address_rain_alarm: GroupAddressesType | None = None,
        group_address_frost_alarm: GroupAddressesType | None = None,
        group_address_wind_alarm: GroupAddressesType | None = None,
        group_address_day_night: GroupAddressesType | None = None,
        group_address_air_pressure: GroupAddressesType | None = None,
        group_address_humidity: GroupAddressesType | None = None,
        create_sensors: bool = False,
        sync_state: bool = True,
        device_updated_cb: DeviceCallbackType | None = None,
    ) -> None:
        """Initialize Weather class."""
        super().__init__(xknx, name, device_updated_cb)

        self._temperature = RemoteValueSensor(
            xknx,
            group_address_state=group_address_temperature,
            sync_state=sync_state,
            value_type="temperature",
            device_name=self.name,
            feature_name="Temperature",
            after_update_cb=self.after_update,
        )

        self._brightness_south = RemoteValueSensor(
            xknx,
            group_address_state=group_address_brightness_south,
            sync_state=sync_state,
            value_type="illuminance",
            device_name=self.name,
            feature_name="Brightness south",
            after_update_cb=self.after_update,
        )

        self._brightness_north = RemoteValueSensor(
            xknx,
            group_address_state=group_address_brightness_north,
            sync_state=sync_state,
            value_type="illuminance",
            device_name=self.name,
            feature_name="Brightness north",
            after_update_cb=self.after_update,
        )

        self._brightness_west = RemoteValueSensor(
            xknx,
            group_address_state=group_address_brightness_west,
            sync_state=sync_state,
            value_type="illuminance",
            device_name=self.name,
            feature_name="Brightness west",
            after_update_cb=self.after_update,
        )

        self._brightness_east = RemoteValueSensor(
            xknx,
            group_address_state=group_address_brightness_east,
            sync_state=sync_state,
            value_type="illuminance",
            device_name=self.name,
            feature_name="Brightness east",
            after_update_cb=self.after_update,
        )

        self._wind_speed = RemoteValueSensor(
            xknx,
            group_address_state=group_address_wind_speed,
            sync_state=sync_state,
            value_type="wind_speed_ms",
            device_name=self.name,
            feature_name="Wind speed",
            after_update_cb=self.after_update,
        )

        self._wind_bearing = RemoteValueSensor(
            xknx,
            group_address_state=group_address_wind_bearing,
            sync_state=sync_state,
            value_type="angle",
            device_name=self.name,
            feature_name="Wind bearing",
            after_update_cb=self.after_update,
        )

        self._rain_alarm = RemoteValueSwitch(
            xknx,
            group_address_state=group_address_rain_alarm,
            device_name=self.name,
            feature_name="Rain alarm",
            after_update_cb=self.after_update,
        )

        self._frost_alarm = RemoteValueSwitch(
            xknx,
            group_address_state=group_address_frost_alarm,
            device_name=self.name,
            feature_name="Frost alarm",
            after_update_cb=self.after_update,
        )

        self._wind_alarm = RemoteValueSwitch(
            xknx,
            group_address_state=group_address_wind_alarm,
            device_name=self.name,
            feature_name="Wind alarm",
            after_update_cb=self.after_update,
        )

        self._day_night = RemoteValueSwitch(
            xknx,
            group_address_state=group_address_day_night,
            device_name=self.name,
            feature_name="Day/Night",
            after_update_cb=self.after_update,
        )

        self._air_pressure = RemoteValueSensor(
            xknx,
            group_address_state=group_address_air_pressure,
            sync_state=sync_state,
            value_type="pressure_2byte",
            device_name=self.name,
            feature_name="Air pressure",
            after_update_cb=self.after_update,
        )

        self._humidity = RemoteValueSensor(
            xknx,
            group_address_state=group_address_humidity,
            sync_state=sync_state,
            value_type="humidity",
            device_name=self.name,
            feature_name="Humidity",
            after_update_cb=self.after_update,
        )

        if create_sensors:
            self.create_sensors()

    def _iter_remote_values(self) -> Iterator[RemoteValue[Any, Any]]:
        """Iterate the devices remote values."""
        yield self._temperature
        yield self._brightness_south
        yield self._brightness_north
        yield self._brightness_east
        yield self._brightness_west
        yield self._wind_speed
        yield self._wind_bearing
        yield self._rain_alarm
        yield self._wind_alarm
        yield self._frost_alarm
        yield self._day_night
        yield self._air_pressure
        yield self._humidity

    @property
    def unique_id(self) -> str | None:
        """Return unique id for this device."""
        return f"{self._temperature.group_address_state}"

    async def process_group_write(self, telegram: "Telegram") -> None:
        """Process incoming and outgoing GROUP WRITE telegram."""
        for remote_value in self._iter_remote_values():
            await remote_value.process(telegram)

    @property
    def temperature(self) -> float | None:
        """Return current temperature."""
        return self._temperature.value  # type: ignore

    @property
    def brightness_south(self) -> float:
        """Return brightness south."""
        if self._brightness_south.value is not None:
            return self._brightness_south.value  # type: ignore
        return 0.0

    @property
    def brightness_north(self) -> float:
        """Return brightness north."""
        if self._brightness_north.value is not None:
            return self._brightness_north.value  # type: ignore
        return 0.0

    @property
    def brightness_east(self) -> float:
        """Return brightness east."""
        if self._brightness_east.value is not None:
            return self._brightness_east.value  # type: ignore
        return 0.0

    @property
    def brightness_west(self) -> float:
        """Return brightness west."""
        if self._brightness_west.value is not None:
            return self._brightness_west.value  # type: ignore
        return 0.0

    @property
    def wind_speed(self) -> float | None:
        """Return wind speed in m/s."""
        return self._wind_speed.value  # type: ignore

    @property
    def wind_bearing(self) -> int | None:
        """Return wind bearing in °."""
        return self._wind_bearing.value  # type: ignore

    @property
    def rain_alarm(self) -> bool | None:
        """Return True if rain alarm False if not."""
        return self._rain_alarm.value

    @property
    def wind_alarm(self) -> bool | None:
        """Return True if wind alarm False if not."""
        return self._wind_alarm.value

    @property
    def frost_alarm(self) -> bool | None:
        """Return True if frost alarm False if not."""
        return self._frost_alarm.value

    @property
    def day_night(self) -> bool | None:
        """Return day or night."""
        return self._day_night.value

    @property
    def air_pressure(self) -> float | None:
        """Return pressure in Pa."""
        return self._air_pressure.value  # type: ignore

    @property
    def humidity(self) -> float | None:
        """Return humidity in %."""
        return self._humidity.value  # type: ignore

    @property
    def max_brightness(self) -> float:
        """Return highest illuminance from all sensors."""
        return max(
            self.brightness_west,
            self.brightness_south,
            self.brightness_north,
            self.brightness_east,
        )

    def create_sensors(self) -> None:
        """Expose sensors to xknx."""
        for suffix, group_address in (
            ("_rain_alarm", self._rain_alarm.group_address_state),
            ("_wind_alarm", self._wind_alarm.group_address_state),
            ("_frost_alarm", self._frost_alarm.group_address_state),
            ("_day_night", self._day_night.group_address_state),
        ):
            if group_address is not None:
                BinarySensor(
                    self.xknx,
                    name=self.name + suffix,
                    group_address_state=group_address,
                )

        for suffix, group_address, value_type in (
            (
                "_temperature",
                self._temperature.group_address_state,
                "temperature",
            ),
            (
                "_brightness_south",
                self._brightness_south.group_address_state,
                "illuminance",
            ),
            (
                "_brightness_north",
                self._brightness_north.group_address_state,
                "illuminance",
            ),
            (
                "_brightness_west",
                self._brightness_west.group_address_state,
                "illuminance",
            ),
            (
                "_brightness_east",
                self._brightness_east.group_address_state,
                "illuminance",
            ),
            (
                "_wind_speed",
                self._wind_speed.group_address_state,
                "wind_speed_ms",
            ),
            (
                "_wind_bearing",
                self._wind_bearing.group_address_state,
                "angle",
            ),
            (
                "_air_pressure",
                self._air_pressure.group_address_state,
                "pressure",
            ),
            (
                "_humidity",
                self._humidity.group_address_state,
                "humidity",
            ),
        ):
            if group_address is not None:
                Sensor(
                    self.xknx,
                    name=self.name + suffix,
                    group_address_state=group_address,
                    value_type=value_type,
                )

    def ha_current_state(self, current_date: date = date.today()) -> WeatherCondition:
        """Return the current state for home assistant."""

        def _get_season(now: date) -> Season:
            """Return winter or summer."""
            if isinstance(now, datetime):
                now = now.date()
            now = now.replace(year=YEAR)
            return next(
                season for season, (start, end) in SEASONS if start <= now <= end
            )

        if self.wind_alarm and self.rain_alarm:
            return WeatherCondition.LIGHTNING_RAINY

        if self.frost_alarm and self.rain_alarm:
            return WeatherCondition.SNOWY_RAINY

        if self.rain_alarm:
            return WeatherCondition.RAINY

        if self.wind_alarm:
            return WeatherCondition.WINDY

        current_season: Season = _get_season(current_date)
        _season: Season
        function: Callable[[float], bool]
        result: WeatherCondition
        for _season, function, result in ILLUMINANCE_MAPPING:
            if _season == current_season and function(self.max_brightness):
                return result

        if self.day_night is False:
            return WeatherCondition.CLEAR_NIGHT

        return WeatherCondition.EXCEPTIONAL

    def __str__(self) -> str:
        """Return object as readable string."""
        return (
            '<Weather name="{}" '
            "temperature={} brightness_south={} brightness_north={} brightness_west={} "
            "brightness_east={} wind_speed={} wind_bearing={} rain_alarm={} "
            "wind_alarm={} frost_alarm={} day_night={} "
            "air_pressure={} humidity={} />".format(
                self.name,
                self._temperature.group_addr_str(),
                self._brightness_south.group_addr_str(),
                self._brightness_north.group_addr_str(),
                self._brightness_west.group_addr_str(),
                self._brightness_east.group_addr_str(),
                self._wind_speed.group_addr_str(),
                self._wind_bearing.group_addr_str(),
                self._rain_alarm.group_addr_str(),
                self._wind_alarm.group_addr_str(),
                self._frost_alarm.group_addr_str(),
                self._day_night.group_addr_str(),
                self._air_pressure.group_addr_str(),
                self._humidity.group_addr_str(),
            )
        )
Пример #7
0
class Cover(Device):
    """Class for managing a cover."""

    # pylint: disable=too-many-instance-attributes
    # pylint: disable=too-many-public-methods
    # pylint: disable=too-many-locals

    # Average typical travel time of a cover
    DEFAULT_TRAVEL_TIME_DOWN = 22
    DEFAULT_TRAVEL_TIME_UP = 22

    def __init__(self,
                 xknx,
                 name,
                 group_address_long=None,
                 group_address_short=None,
                 group_address_stop=None,
                 group_address_position=None,
                 group_address_position_state=None,
                 group_address_angle=None,
                 group_address_angle_state=None,
                 travel_time_down=DEFAULT_TRAVEL_TIME_DOWN,
                 travel_time_up=DEFAULT_TRAVEL_TIME_UP,
                 invert_position=False,
                 invert_angle=False,
                 device_updated_cb=None):
        """Initialize Cover class."""
        # pylint: disable=too-many-arguments
        super().__init__(xknx, name, device_updated_cb)
        # self.after_update for position changes is called after updating the
        # travelcalculator (in process_group_write and set_*) - angle changes
        # are updated from RemoteValue objects
        self.updown = RemoteValueUpDown(xknx,
                                        group_address_long,
                                        device_name=self.name,
                                        after_update_cb=None)

        self.step = RemoteValueStep(xknx,
                                    group_address_short,
                                    device_name=self.name,
                                    after_update_cb=self.after_update)

        self.stop_ = RemoteValueSwitch(xknx,
                                       group_address=group_address_stop,
                                       device_name=self.name,
                                       after_update_cb=None)

        position_range_from = 100 if invert_position else 0
        position_range_to = 0 if invert_position else 100
        self.position = RemoteValueScaling(xknx,
                                           group_address_position,
                                           group_address_position_state,
                                           device_name=self.name,
                                           feature_name="Position",
                                           after_update_cb=None,
                                           range_from=position_range_from,
                                           range_to=position_range_to)

        angle_range_from = 100 if invert_angle else 0
        angle_range_to = 0 if invert_angle else 100
        self.angle = RemoteValueScaling(xknx,
                                        group_address_angle,
                                        group_address_angle_state,
                                        device_name=self.name,
                                        feature_name="Tilt angle",
                                        after_update_cb=self.after_update,
                                        range_from=angle_range_from,
                                        range_to=angle_range_to)

        self.travel_time_down = travel_time_down
        self.travel_time_up = travel_time_up

        self.travelcalculator = TravelCalculator(travel_time_down,
                                                 travel_time_up)

    def _iter_remote_values(self):
        """Iterate the devices RemoteValue classes."""
        yield from (self.updown, self.step, self.stop_, self.position,
                    self.angle)

    @classmethod
    def from_config(cls, xknx, name, config):
        """Initialize object from configuration structure."""
        group_address_long = \
            config.get('group_address_long')
        group_address_short = \
            config.get('group_address_short')
        group_address_stop = \
            config.get('group_address_stop')
        group_address_position = \
            config.get('group_address_position')
        group_address_position_state = \
            config.get('group_address_position_state')
        group_address_angle = \
            config.get('group_address_angle')
        group_address_angle_state = \
            config.get('group_address_angle_state')
        travel_time_down = \
            config.get('travel_time_down', cls.DEFAULT_TRAVEL_TIME_DOWN)
        travel_time_up = \
            config.get('travel_time_up', cls.DEFAULT_TRAVEL_TIME_UP)
        invert_position = \
            config.get('invert_position', False)
        invert_angle = \
            config.get('invert_angle', False)

        return cls(xknx,
                   name,
                   group_address_long=group_address_long,
                   group_address_short=group_address_short,
                   group_address_stop=group_address_stop,
                   group_address_position=group_address_position,
                   group_address_position_state=group_address_position_state,
                   group_address_angle=group_address_angle,
                   group_address_angle_state=group_address_angle_state,
                   travel_time_down=travel_time_down,
                   travel_time_up=travel_time_up,
                   invert_position=invert_position,
                   invert_angle=invert_angle)

    def __str__(self):
        """Return object as readable string."""
        return '<Cover name="{0}" ' \
            'updown="{1}" ' \
            'step="{2}" ' \
            'stop="{3}" ' \
            'position="{4}" ' \
            'angle="{5}" '\
            'travel_time_down="{6}" ' \
            'travel_time_up="{7}" />' \
            .format(
                self.name,
                self.updown.group_addr_str(),
                self.step.group_addr_str(),
                self.stop_.group_addr_str(),
                self.position.group_addr_str(),
                self.angle.group_addr_str(),
                self.travel_time_down,
                self.travel_time_up)

    async def set_down(self):
        """Move cover down."""
        await self.updown.down()
        self.travelcalculator.start_travel_down()
        await self.after_update()

    async def set_up(self):
        """Move cover up."""
        await self.updown.up()
        self.travelcalculator.start_travel_up()
        await self.after_update()

    async def set_short_down(self):
        """Move cover short down."""
        await self.step.increase()

    async def set_short_up(self):
        """Move cover short up."""
        await self.step.decrease()

    async def stop(self):
        """Stop cover."""
        if self.stop_.writable:
            await self.stop_.on()
        elif self.step.writable:
            await self.step.increase()
        else:
            self.xknx.logger.warning('Stop not supported for device %s',
                                     self.get_name())
            return
        self.travelcalculator.stop()
        await self.after_update()

    async def set_position(self, position):
        """Move cover to a desginated postion."""
        if not self.position.writable:
            # No direct positioning group address defined
            # fully open or close is always possible even if current position is not known
            current_position = self.current_position()
            if current_position is None:
                if position == self.travelcalculator.position_open:
                    await self.updown.up()
                elif position == self.travelcalculator.position_closed:
                    await self.updown.down()
                else:
                    self.xknx.logger.warning(
                        "Current position unknown. Initialize cover by moving to end position."
                    )
                    return
            elif position < current_position:
                await self.updown.up()
            elif position > current_position:
                await self.updown.down()
        else:
            await self.position.set(position)
        self.travelcalculator.start_travel(position)
        await self.after_update()

    async def set_angle(self, angle):
        """Move cover to designated angle."""
        if not self.supports_angle:
            self.xknx.logger.warning('Angle not supported for device %s',
                                     self.get_name())
            return
        await self.angle.set(angle)

    async def auto_stop_if_necessary(self):
        """Do auto stop if necessary."""
        # If device does not support auto_positioning,
        # we have to stop the device when position is reached.
        # unless device was traveling to fully open
        # or fully closed state
        if (self.supports_stop and not self.position.writable
                and self.position_reached() and not self.is_open()
                and not self.is_closed()):
            await self.stop()

    async def do(self, action):
        """Execute 'do' commands."""
        if action == "up":
            await self.set_up()
        elif action == "short_up":
            await self.set_short_up()
        elif action == "down":
            await self.set_down()
        elif action == "short_down":
            await self.set_short_down()
        elif action == "stop":
            await self.stop()
        else:
            self.xknx.logger.warning(
                "Could not understand action %s for device %s", action,
                self.get_name())

    async def sync(self):
        """Read states of device from KNX bus."""
        await self.position.read_state()
        await self.angle.read_state()

    async def process_group_write(self, telegram):
        """Process incoming GROUP WRITE telegram."""
        if await self.updown.process(telegram):
            if self.updown.value == RemoteValueUpDown.Direction.UP:
                self.travelcalculator.start_travel_up()
            else:
                self.travelcalculator.start_travel_down()
            # call after_update to account for travelcalculator changes
            await self.after_update()
        # stop from bus
        if await self.stop_.process(telegram) or await self.step.process(
                telegram):
            if self.is_traveling():
                self.travelcalculator.stop()
                await self.after_update()

        if await self.position.process(telegram):
            # distinction between new target position and position update from bus
            if telegram.group_address == self.position.group_address_state:
                if self.is_traveling():
                    self.travelcalculator.update_position(self.position.value)
                else:
                    self.travelcalculator.set_position(self.position.value)
            else:
                self.travelcalculator.start_travel(self.position.value)
            await self.after_update()

        await self.angle.process(telegram)

    def current_position(self):
        """Return current position of cover."""
        return self.travelcalculator.current_position()

    def current_angle(self):
        """Return current tilt angle of cover."""
        return self.angle.value

    def is_traveling(self):
        """Return if cover is traveling at the moment."""
        return self.travelcalculator.is_traveling()

    def position_reached(self):
        """Return if cover has reached its final position."""
        return self.travelcalculator.position_reached()

    def is_open(self):
        """Return if cover is open."""
        return self.travelcalculator.is_open()

    def is_closed(self):
        """Return if cover is closed."""
        return self.travelcalculator.is_closed()

    def is_opening(self):
        """Return if the cover is opening or not."""
        return self.travelcalculator.is_opening()

    def is_closing(self):
        """Return if the cover is closing or not."""
        return self.travelcalculator.is_closing()

    @property
    def supports_stop(self):
        """Return if cover supports manual stopping."""
        return self.stop_.writable or self.step.writable

    @property
    def supports_position(self):
        """Return if cover supports direct positioning."""
        return self.position.initialized

    @property
    def supports_angle(self):
        """Return if cover supports tilt angle."""
        return self.angle.initialized

    def __eq__(self, other):
        """Equal operator."""
        return self.__dict__ == other.__dict__
Пример #8
0
class Switch(Device):
    """Class for managing a switch."""
    def __init__(
        self,
        xknx: XKNX,
        name: str,
        group_address: GroupAddressesType | None = None,
        group_address_state: GroupAddressesType | None = None,
        invert: bool = False,
        reset_after: float | None = None,
        device_updated_cb: DeviceCallbackType | None = None,
    ):
        """Initialize Switch class."""
        super().__init__(xknx, name, device_updated_cb)

        self.reset_after = reset_after
        self._reset_task: asyncio.Task[None] | None = None

        self.switch = RemoteValueSwitch(
            xknx,
            group_address,
            group_address_state,
            invert=invert,
            device_name=self.name,
            after_update_cb=self.after_update,
        )

    def _iter_remote_values(self) -> Iterator[RemoteValueSwitch]:
        """Iterate the devices RemoteValue classes."""
        yield self.switch

    def __del__(self) -> None:
        """Destructor. Cleaning up if this was not done before."""
        if self._reset_task:
            try:
                self._reset_task.cancel()
            except RuntimeError:
                pass
        super().__del__()

    @property
    def unique_id(self) -> str | None:
        """Return unique id for this device."""
        return f"{self.switch.group_address}"

    @property
    def state(self) -> bool | None:
        """Return the current switch state of the device."""
        return self.switch.value

    async def set_on(self) -> None:
        """Switch on switch."""
        await self.switch.on()

    async def set_off(self) -> None:
        """Switch off switch."""
        await self.switch.off()

    async def process_group_write(self, telegram: "Telegram") -> None:
        """Process incoming and outgoing GROUP WRITE telegram."""
        if await self.switch.process(telegram):
            if self.reset_after is not None and self.switch.value:
                if self._reset_task:
                    self._reset_task.cancel()
                self._reset_task = asyncio.create_task(
                    self._reset_state(self.reset_after))

    async def _reset_state(self, wait_seconds: float) -> None:
        await asyncio.sleep(wait_seconds)
        await self.set_off()

    def __str__(self) -> str:
        """Return object as readable string."""
        return '<Switch name="{}" switch={} />'.format(
            self.name, self.switch.group_addr_str())
Пример #9
0
class Light(Device):
    """Class for managing a light."""

    # pylint: disable=too-many-locals
    DEFAULT_MIN_KELVIN = 2700  # 370 mireds
    DEFAULT_MAX_KELVIN = 6000  # 166 mireds

    def __init__(self,
                 xknx,
                 name,
                 group_address_switch=None,
                 group_address_switch_state=None,
                 group_address_brightness=None,
                 group_address_brightness_state=None,
                 group_address_color=None,
                 group_address_color_state=None,
                 group_address_rgbw=None,
                 group_address_rgbw_state=None,
                 group_address_tunable_white=None,
                 group_address_tunable_white_state=None,
                 group_address_color_temperature=None,
                 group_address_color_temperature_state=None,
                 min_kelvin=None,
                 max_kelvin=None,
                 device_updated_cb=None):
        """Initialize Light class."""
        # pylint: disable=too-many-arguments
        super().__init__(xknx, name, device_updated_cb)

        self.switch = RemoteValueSwitch(xknx,
                                        group_address_switch,
                                        group_address_switch_state,
                                        device_name=self.name,
                                        feature_name="State",
                                        after_update_cb=self.after_update)

        self.brightness = RemoteValueScaling(xknx,
                                             group_address_brightness,
                                             group_address_brightness_state,
                                             device_name=self.name,
                                             feature_name="Brightness",
                                             after_update_cb=self.after_update,
                                             range_from=0,
                                             range_to=255)

        self.color = RemoteValueColorRGB(xknx,
                                         group_address_color,
                                         group_address_color_state,
                                         device_name=self.name,
                                         after_update_cb=self.after_update)

        self.rgbw = RemoteValueColorRGBW(xknx,
                                         group_address_rgbw,
                                         group_address_rgbw_state,
                                         device_name=self.name,
                                         after_update_cb=self.after_update)

        self.tunable_white = RemoteValueScaling(
            xknx,
            group_address_tunable_white,
            group_address_tunable_white_state,
            device_name=self.name,
            feature_name="Tunable white",
            after_update_cb=self.after_update,
            range_from=0,
            range_to=255)

        self.color_temperature = RemoteValueDpt2ByteUnsigned(
            xknx,
            group_address_color_temperature,
            group_address_color_temperature_state,
            device_name=self.name,
            feature_name="Color temperature",
            after_update_cb=self.after_update)

        self.min_kelvin = min_kelvin
        self.max_kelvin = max_kelvin

    def _iter_remote_values(self):
        """Iterate the devices RemoteValue classes."""
        yield from (self.switch, self.brightness, self.color, self.rgbw,
                    self.tunable_white, self.color_temperature)

    @property
    def supports_brightness(self):
        """Return if light supports brightness."""
        return self.brightness.initialized

    @property
    def supports_color(self):
        """Return if light supports color."""
        return self.color.initialized

    @property
    def supports_rgbw(self):
        """Return if light supports RGBW."""
        return self.rgbw.initialized

    @property
    def supports_tunable_white(self):
        """Return if light supports tunable white / relative color temperature."""
        return self.tunable_white.initialized

    @property
    def supports_color_temperature(self):
        """Return if light supports absolute color temperature."""
        return self.color_temperature.initialized

    @classmethod
    def from_config(cls, xknx, name, config):
        """Initialize object from configuration structure."""
        group_address_switch = \
            config.get('group_address_switch')
        group_address_switch_state = \
            config.get('group_address_switch_state')
        group_address_brightness = \
            config.get('group_address_brightness')
        group_address_brightness_state = \
            config.get('group_address_brightness_state')
        group_address_color = \
            config.get('group_address_color')
        group_address_color_state = \
            config.get('group_address_color_state')
        group_address_rgbw = \
            config.get('group_address_rgbw')
        group_address_rgbw_state = \
            config.get('group_address_rgbw_state')
        group_address_tunable_white = \
            config.get('group_address_tunable_white')
        group_address_tunable_white_state = \
            config.get('group_address_tunable_white_state')
        group_address_color_temperature = \
            config.get('group_address_color_temperature')
        group_address_color_temperature_state = \
            config.get('group_address_color_temperature_state')
        min_kelvin = \
            config.get('min_kelvin', Light.DEFAULT_MIN_KELVIN)
        max_kelvin = \
            config.get('max_kelvin', Light.DEFAULT_MAX_KELVIN)

        return cls(
            xknx,
            name,
            group_address_switch=group_address_switch,
            group_address_switch_state=group_address_switch_state,
            group_address_brightness=group_address_brightness,
            group_address_brightness_state=group_address_brightness_state,
            group_address_color=group_address_color,
            group_address_color_state=group_address_color_state,
            group_address_rgbw=group_address_rgbw,
            group_address_rgbw_state=group_address_rgbw_state,
            group_address_tunable_white=group_address_tunable_white,
            group_address_tunable_white_state=group_address_tunable_white_state,
            group_address_color_temperature=group_address_color_temperature,
            group_address_color_temperature_state=
            group_address_color_temperature_state,
            min_kelvin=min_kelvin,
            max_kelvin=max_kelvin)

    def __str__(self):
        """Return object as readable string."""
        str_brightness = '' if not self.supports_brightness else \
            ' brightness="{0}"'.format(
                self.brightness.group_addr_str())

        str_color = '' if not self.supports_color else \
            ' color="{0}"'.format(
                self.color.group_addr_str())

        str_rgbw = '' if not self.supports_rgbw else \
            ' rgbw="{0}"'.format(
                self.rgbw.group_addr_str())

        str_tunable_white = '' if not self.supports_tunable_white else \
            ' tunable white="{0}"'.format(
                self.tunable_white.group_addr_str())

        str_color_temperature = '' if not self.supports_color_temperature else \
            ' color temperature="{0}"'.format(
                self.color_temperature.group_addr_str())

        return '<Light name="{0}" ' \
            'switch="{1}"{2}{3}{4}{5}{6} />' \
            .format(
                self.name,
                self.switch.group_addr_str(),
                str_brightness,
                str_color,
                str_rgbw,
                str_tunable_white,
                str_color_temperature)

    @property
    def state(self):
        """Return the current switch state of the device."""
        # None will return False
        return bool(self.switch.value)

    async def set_on(self):
        """Switch light on."""
        await self.switch.on()

    async def set_off(self):
        """Switch light off."""
        await self.switch.off()

    @property
    def current_brightness(self):
        """Return current brightness of light."""
        return self.brightness.value

    async def set_brightness(self, brightness):
        """Set brightness of light."""
        if not self.supports_brightness:
            self.xknx.logger.warning("Dimming not supported for device %s",
                                     self.get_name())
            return
        await self.brightness.set(brightness)

    @property
    def current_color(self):
        """
        Return current color of light.

        If the device supports RGBW, get the current RGB+White values instead.
        """
        if self.supports_rgbw:
            if not self.rgbw.value:
                return None, None
            return self.rgbw.value[:3], self.rgbw.value[3]
        return self.color.value, None

    async def set_color(self, color, white=None):
        """
        Set color of a light device.

        If also the white value is given and the device supports RGBW,
        set all four values.
        """
        if white is not None:
            if self.supports_rgbw:
                await self.rgbw.set(list(color) + [white])
                return
            self.xknx.logger.warning("RGBW not supported for device %s",
                                     self.get_name())
        else:
            if self.supports_color:
                await self.color.set(color)
                return
            self.xknx.logger.warning("Colors not supported for device %s",
                                     self.get_name())

    @property
    def current_tunable_white(self):
        """Return current relative color temperature of light."""
        return self.tunable_white.value

    async def set_tunable_white(self, tunable_white):
        """Set relative color temperature of light."""
        if not self.supports_tunable_white:
            self.xknx.logger.warning(
                "Tunable white not supported for device %s", self.get_name())
            return
        await self.tunable_white.set(tunable_white)

    @property
    def current_color_temperature(self):
        """Return current absolute color temperature of light."""
        return self.color_temperature.value

    async def set_color_temperature(self, color_temperature):
        """Set absolute color temperature of light."""
        if not self.supports_color_temperature:
            self.xknx.logger.warning(
                "Absolute Color Temperature not supported for device %s",
                self.get_name())
            return
        await self.color_temperature.set(color_temperature)

    async def do(self, action):
        """Execute 'do' commands."""
        if action == "on":
            await self.set_on()
        elif action == "off":
            await self.set_off()
        elif action.startswith("brightness:"):
            await self.set_brightness(int(action[11:]))
        elif action.startswith("tunable_white:"):
            await self.set_tunable_white(int(action[14:]))
        elif action.startswith("color_temperature:"):
            await self.set_color_temperature(int(action[18:]))
        else:
            self.xknx.logger.warning(
                "Could not understand action %s for device %s", action,
                self.get_name())

    async def process_group_write(self, telegram):
        """Process incoming GROUP WRITE telegram."""
        for remote_value in self._iter_remote_values():
            await remote_value.process(telegram)

    def __eq__(self, other):
        """Equal operator."""
        return self.__dict__ == other.__dict__
Пример #10
0
class Switch(Device):
    """Class for managing a switch."""
    def __init__(
        self,
        xknx,
        name: str,
        group_address=None,
        group_address_state=None,
        invert: Optional[bool] = False,
        reset_after: Optional[float] = None,
        device_updated_cb=None,
    ):
        """Initialize Switch class."""
        # pylint: disable=too-many-arguments
        super().__init__(xknx, name, device_updated_cb)

        self.reset_after = reset_after
        self._reset_task = None

        self.switch = RemoteValueSwitch(
            xknx,
            group_address,
            group_address_state,
            invert=invert,
            device_name=self.name,
            after_update_cb=self.after_update,
        )

    def _iter_remote_values(self):
        """Iterate the devices RemoteValue classes."""
        yield self.switch

    def __del__(self):
        """Destructor. Cleaning up if this was not done before."""
        if self._reset_task:
            self._reset_task.cancel()

    @classmethod
    def from_config(cls, xknx, name, config):
        """Initialize object from configuration structure."""
        group_address = config.get("group_address")
        group_address_state = config.get("group_address_state")
        invert = config.get("invert")
        reset_after = config.get("reset_after")

        return cls(
            xknx,
            name,
            group_address=group_address,
            group_address_state=group_address_state,
            invert=invert,
            reset_after=reset_after,
        )

    @property
    def state(self) -> Optional[bool]:
        """Return the current switch state of the device."""
        return self.switch.value

    async def set_on(self):
        """Switch on switch."""
        await self.switch.on()

    async def set_off(self):
        """Switch off switch."""
        await self.switch.off()

    async def do(self, action):
        """Execute 'do' commands."""
        if action == "on":
            await self.set_on()
        elif action == "off":
            await self.set_off()
        else:
            logger.warning("Could not understand action %s for device %s",
                           action, self.get_name())

    async def process_group_write(self, telegram):
        """Process incoming and outgoing GROUP WRITE telegram."""
        if await self.switch.process(telegram):
            if self.reset_after is not None and self.switch.value:
                if self._reset_task:
                    self._reset_task.cancel()
                self._reset_task = asyncio.create_task(
                    self._reset_state(self.reset_after))

    async def _reset_state(self, wait_seconds: float):
        await asyncio.sleep(wait_seconds)
        await self.set_off()

    def __str__(self):
        """Return object as readable string."""
        return '<Switch name="{}" switch="{}" />'.format(
            self.name, self.switch.group_addr_str())
Пример #11
0
class BinarySensor(Device):
    """Class for binary sensor."""

    def __init__(
        self,
        xknx: XKNX,
        name: str,
        group_address_state: GroupAddressesType = None,
        invert: bool = False,
        sync_state: bool = True,
        ignore_internal_state: bool = False,
        device_class: str | None = None,
        reset_after: float | None = None,
        context_timeout: float | None = None,
        ha_value_template: Any = None,
        device_updated_cb: DeviceCallbackType | None = None,
    ):
        """Initialize BinarySensor class."""
        super().__init__(xknx, name, device_updated_cb)

        self.device_class = device_class
        self.ha_value_template = ha_value_template
        self.ignore_internal_state = ignore_internal_state or bool(context_timeout)
        self.reset_after = reset_after
        self.state: bool | None = None

        self._context_timeout = context_timeout
        self._count_set_on = 0
        self._count_set_off = 0
        self._last_set: float | None = None
        self._reset_task: asyncio.Task[None] | None = None
        self._context_task: asyncio.Task[None] | None = None
        # TODO: log a warning if reset_after and sync_state are true ? This could cause actions to self-fire.
        self.remote_value = RemoteValueSwitch(
            xknx,
            group_address_state=group_address_state,
            invert=invert,
            sync_state=sync_state,
            device_name=self.name,
            # after_update called internally
            after_update_cb=self._state_from_remote_value,
        )

    def _iter_remote_values(self) -> Iterator[RemoteValueSwitch]:
        """Iterate the devices RemoteValue classes."""
        yield self.remote_value

    @property
    def unique_id(self) -> str | None:
        """Return unique id for this device."""
        return f"{self.remote_value.group_address_state}"

    @property
    def last_telegram(self) -> Telegram | None:
        """Return the last telegram received from the RemoteValue."""
        return self.remote_value.telegram

    def __del__(self) -> None:
        """Destructor. Cleaning up if this was not done before."""
        try:
            if self._reset_task:
                self._reset_task.cancel()
            if self._context_task:
                self._context_task.cancel()
        except RuntimeError:
            pass
        super().__del__()

    async def _state_from_remote_value(self) -> None:
        """Update the internal state from RemoteValue (Callback)."""
        if self.remote_value.value is not None:
            await self._set_internal_state(self.remote_value.value)

    async def _set_internal_state(self, state: bool) -> None:
        """Set the internal state of the device. If state was changed after_update hooks and connected Actions are executed."""
        if state != self.state or self.ignore_internal_state:
            self.state = state

            if self.ignore_internal_state and self._context_timeout:
                self.bump_and_get_counter(state)
                if self._context_task:
                    self._context_task.cancel()
                self._context_task = asyncio.create_task(
                    self._counter_task(self._context_timeout)
                )
            else:
                await self._trigger_callbacks()

    async def _counter_task(self, wait_seconds: float) -> None:
        """Trigger after 1 second to prevent double triggers."""
        await asyncio.sleep(wait_seconds)
        await self._trigger_callbacks()

        self._count_set_on = 0
        self._count_set_off = 0

        await self.after_update()

    async def _trigger_callbacks(self) -> None:
        """Trigger callbacks for device if any."""
        await self.after_update()

    @property
    def counter(self) -> int | None:
        """Return current counter for sensor."""
        if self._context_timeout:
            return self._count_set_on if self.state else self._count_set_off
        return None

    def bump_and_get_counter(self, state: bool) -> int:
        """Bump counter and return the number of times a state was set to the same value within CONTEXT_TIMEOUT."""

        def within_same_context() -> bool:
            """Check if state change was within same context (e.g. 'Button was pressed twice')."""
            if self._last_set is None:
                self._last_set = time.time()
                return False
            new_set_time = time.time()
            time_diff = new_set_time - self._last_set
            self._last_set = new_set_time
            return time_diff < cast(float, self._context_timeout)

        if within_same_context():
            if state:
                self._count_set_on = self._count_set_on + 1
                return self._count_set_on
            self._count_set_off = self._count_set_off + 1
            return self._count_set_off

        if state:
            self._count_set_on = 1
            self._count_set_off = 0
        else:
            self._count_set_on = 0
            self._count_set_off = 1
        return 1

    async def process_group_write(self, telegram: "Telegram") -> None:
        """Process incoming and outgoing GROUP WRITE telegram."""
        if await self.remote_value.process(telegram, always_callback=True):
            self._process_reset_after()

    async def process_group_response(self, telegram: "Telegram") -> None:
        """Process incoming GroupValueResponse telegrams."""
        if await self.remote_value.process(telegram, always_callback=False):
            self._process_reset_after()

    def _process_reset_after(self) -> None:
        """Create Task for resetting state if 'reset_after' is configured."""
        if self.reset_after is not None and self.state:
            if self._reset_task:
                self._reset_task.cancel()
            self._reset_task = asyncio.create_task(self._reset_state(self.reset_after))

    async def _reset_state(self, wait_seconds: float) -> None:
        await asyncio.sleep(wait_seconds)
        await self._set_internal_state(False)

    def is_on(self) -> bool:
        """Return if binary sensor is 'on'."""
        return bool(self.state)

    def is_off(self) -> bool:
        """Return if binary sensor is 'off'."""
        return not self.state

    def __str__(self) -> str:
        """Return object as readable string."""
        return '<BinarySensor name="{}" remote_value={} state={} />'.format(
            self.name, self.remote_value.group_addr_str(), self.state.__repr__()
        )
Пример #12
0
class Climate(Device):
    """Class for managing the climate."""

    # pylint: disable=too-many-instance-attributes,invalid-name
    def __init__(
        self,
        xknx: "XKNX",
        name: str,
        group_address_temperature: Optional["GroupAddressableType"] = None,
        group_address_target_temperature: Optional["GroupAddressableType"] = None,
        group_address_target_temperature_state: Optional["GroupAddressableType"] = None,
        group_address_setpoint_shift: Optional["GroupAddressableType"] = None,
        group_address_setpoint_shift_state: Optional["GroupAddressableType"] = None,
        setpoint_shift_mode: SetpointShiftMode = DEFAULT_SETPOINT_SHIFT_MODE,
        setpoint_shift_max: float = DEFAULT_SETPOINT_SHIFT_MAX,
        setpoint_shift_min: float = DEFAULT_SETPOINT_SHIFT_MIN,
        temperature_step: float = DEFAULT_TEMPERATURE_STEP,
        group_address_on_off: Optional["GroupAddressableType"] = None,
        group_address_on_off_state: Optional["GroupAddressableType"] = None,
        on_off_invert: bool = False,
        min_temp: Optional[float] = None,
        max_temp: Optional[float] = None,
        mode: Optional[ClimateMode] = None,
        create_temperature_sensors: bool = False,
        device_updated_cb: Optional[DeviceCallbackType] = None,
    ):
        """Initialize Climate class."""
        # pylint: disable=too-many-arguments, too-many-locals, too-many-branches, too-many-statements
        super().__init__(xknx, name, device_updated_cb)

        self.min_temp = min_temp
        self.max_temp = max_temp
        self.setpoint_shift_min = setpoint_shift_min
        self.setpoint_shift_max = setpoint_shift_max
        self.temperature_step = temperature_step

        self.temperature = RemoteValueTemp(
            xknx,
            group_address_state=group_address_temperature,
            device_name=self.name,
            feature_name="Current temperature",
            after_update_cb=self.after_update,
        )

        self.target_temperature = RemoteValueTemp(
            xknx,
            group_address_target_temperature,
            group_address_target_temperature_state,
            device_name=self.name,
            feature_name="Target temperature",
            after_update_cb=self.after_update,
        )

        self._setpoint_shift: Union[RemoteValueTemp, RemoteValueSetpointShift]
        if setpoint_shift_mode == SetpointShiftMode.DPT9002:
            self._setpoint_shift = RemoteValueTemp(
                xknx,
                group_address_setpoint_shift,
                group_address_setpoint_shift_state,
                device_name=self.name,
                after_update_cb=self.after_update,
            )
        else:
            self._setpoint_shift = RemoteValueSetpointShift(
                xknx,
                group_address_setpoint_shift,
                group_address_setpoint_shift_state,
                device_name=self.name,
                after_update_cb=self.after_update,
                setpoint_shift_step=self.temperature_step,
            )

        self.supports_on_off = (
            group_address_on_off is not None or group_address_on_off_state is not None
        )

        self.on = RemoteValueSwitch(
            xknx,
            group_address_on_off,
            group_address_on_off_state,
            device_name=self.name,
            after_update_cb=self.after_update,
            invert=on_off_invert,
        )

        self.mode = mode

        if create_temperature_sensors:
            self.create_temperature_sensors()

    def _iter_remote_values(self) -> Iterator["RemoteValue[Any]"]:
        """Iterate the devices RemoteValue classes."""
        yield from (
            self.temperature,
            self.target_temperature,
            self._setpoint_shift,
            self.on,
        )

    def create_temperature_sensors(self) -> None:
        """Create temperature sensors."""
        for suffix, group_address, value_type in (
            (
                "temperature",
                self.temperature.group_address_state,
                "temperature",
            ),
            (
                "target temperature",
                self.target_temperature.group_address_state,
                "temperature",
            ),
        ):
            if group_address is not None:
                Sensor(
                    self.xknx,
                    name=self.name + " " + suffix,
                    group_address_state=group_address,
                    value_type=value_type,
                )

    @classmethod
    def from_config(cls, xknx: "XKNX", name: str, config: Any) -> "Climate":
        """Initialize object from configuration structure."""
        # pylint: disable=too-many-locals
        group_address_temperature = config.get("group_address_temperature")
        group_address_target_temperature = config.get(
            "group_address_target_temperature"
        )
        group_address_target_temperature_state = config.get(
            "group_address_target_temperature_state"
        )
        group_address_setpoint_shift = config.get("group_address_setpoint_shift")
        group_address_setpoint_shift_state = config.get(
            "group_address_setpoint_shift_state"
        )
        setpoint_shift_mode = config.get(
            "setpoint_shift_mode", DEFAULT_SETPOINT_SHIFT_MODE
        )
        setpoint_shift_max = config.get(
            "setpoint_shift_max", DEFAULT_SETPOINT_SHIFT_MAX
        )
        setpoint_shift_min = config.get(
            "setpoint_shift_min", DEFAULT_SETPOINT_SHIFT_MIN
        )
        temperature_step = config.get("temperature_step", DEFAULT_TEMPERATURE_STEP)
        group_address_on_off = config.get("group_address_on_off")
        group_address_on_off_state = config.get("group_address_on_off_state")
        on_off_invert = config.get("on_off_invert", False)
        min_temp = config.get("min_temp")
        max_temp = config.get("max_temp")

        climate_mode = None
        if "mode" in config:
            climate_mode = ClimateMode.from_config(
                xknx=xknx, name=f"{name}_mode", config=config["mode"]
            )

        return cls(
            xknx,
            name,
            group_address_temperature=group_address_temperature,
            group_address_target_temperature=group_address_target_temperature,
            group_address_target_temperature_state=group_address_target_temperature_state,
            group_address_setpoint_shift=group_address_setpoint_shift,
            group_address_setpoint_shift_state=group_address_setpoint_shift_state,
            setpoint_shift_mode=setpoint_shift_mode,
            setpoint_shift_max=setpoint_shift_max,
            setpoint_shift_min=setpoint_shift_min,
            temperature_step=temperature_step,
            group_address_on_off=group_address_on_off,
            group_address_on_off_state=group_address_on_off_state,
            on_off_invert=on_off_invert,
            min_temp=min_temp,
            max_temp=max_temp,
            mode=climate_mode,
        )

    def has_group_address(self, group_address: "GroupAddress") -> bool:
        """Test if device has given group address."""
        if self.mode is not None and self.mode.has_group_address(group_address):
            return True
        return super().has_group_address(group_address)

    @property
    def is_on(self) -> bool:
        """Return power status."""
        # None will return False
        return bool(self.on.value)

    async def turn_on(self) -> None:
        """Set power status to on."""
        await self.on.on()

    async def turn_off(self) -> None:
        """Set power status to off."""
        await self.on.off()

    @property
    def initialized_for_setpoint_shift_calculations(self) -> bool:
        """Test if object is initialized for setpoint shift calculations."""
        if not self._setpoint_shift.initialized:
            return False
        if self._setpoint_shift.value is None:
            return False
        if not self.target_temperature.initialized:
            return False
        if self.target_temperature.value is None:
            return False
        return True

    async def set_target_temperature(self, target_temperature: float) -> None:
        """Send new target temperature or setpoint_shift to KNX bus."""
        if self.base_temperature is not None:
            # implies initialized_for_setpoint_shift_calculations
            temperature_delta = target_temperature - self.base_temperature
            await self.set_setpoint_shift(temperature_delta)
        else:
            validated_temp = self.validate_value(
                target_temperature, self.min_temp, self.max_temp
            )
            await self.target_temperature.set(validated_temp)

    @property
    def base_temperature(self) -> Optional[float]:
        """
        Return the base temperature when setpoint_shift is initialized.

        Base temperature is the default temperature (setpoint-shift=0) for the active climate mode.
        As this value is usually not available via KNX, we have to derive this from the current
        target temperature and the current set point shift.
        """
        if self.initialized_for_setpoint_shift_calculations:
            return cast(float, self.target_temperature.value - self.setpoint_shift)
        return None

    @property
    def setpoint_shift(self) -> Optional[float]:
        """Return current offset from base temperature in Kelvin."""
        return self._setpoint_shift.value  # type: ignore

    def validate_value(
        self, value: float, min_value: Optional[float], max_value: Optional[float]
    ) -> float:
        """Check boundaries of temperature and return valid temperature value."""
        if (min_value is not None) and (value < min_value):
            logger.warning("Min value exceeded at %s: %s", self.name, value)
            return min_value
        if (max_value is not None) and (value > max_value):
            logger.warning("Max value exceeded at %s: %s", self.name, value)
            return max_value
        return value

    async def set_setpoint_shift(self, offset: float) -> None:
        """Send new temperature offset to KNX bus."""
        validated_offset = self.validate_value(
            offset, self.setpoint_shift_min, self.setpoint_shift_max
        )
        base_temperature = self.base_temperature
        await self._setpoint_shift.set(validated_offset)
        # broadcast new target temperature and set internally
        if self.target_temperature.writable and base_temperature is not None:
            await self.target_temperature.set(base_temperature + validated_offset)

    @property
    def target_temperature_max(self) -> Optional[float]:
        """Return the highest possible target temperature."""
        if self.max_temp is not None:
            return self.max_temp
        if self.base_temperature is not None:
            # implies initialized_for_setpoint_shift_calculations
            return self.base_temperature + self.setpoint_shift_max
        return None

    @property
    def target_temperature_min(self) -> Optional[float]:
        """Return the lowest possible target temperature."""
        if self.min_temp is not None:
            return self.min_temp
        if self.base_temperature is not None:
            # implies initialized_for_setpoint_shift_calculations
            return self.base_temperature + self.setpoint_shift_min
        return None

    async def process_group_write(self, telegram: "Telegram") -> None:
        """Process incoming and outgoing GROUP WRITE telegram."""
        for remote_value in self._iter_remote_values():
            await remote_value.process(telegram)
        if self.mode is not None:
            await self.mode.process_group_write(telegram)

    async def sync(self, wait_for_result: bool = False) -> None:
        """Read states of device from KNX bus."""
        await super().sync(wait_for_result=wait_for_result)
        if self.mode is not None:
            await self.mode.sync(wait_for_result=wait_for_result)

    def __str__(self) -> str:
        """Return object as readable string."""
        return (
            '<Climate name="{}" '
            'temperature="{}" '
            'target_temperature="{}" '
            'temperature_step="{}" '
            'setpoint_shift="{}" '
            'setpoint_shift_max="{}" '
            'setpoint_shift_min="{}" '
            'group_address_on_off="{}" '
            "/>".format(
                self.name,
                self.temperature.group_addr_str(),
                self.target_temperature.group_addr_str(),
                self.temperature_step,
                self._setpoint_shift.group_addr_str(),
                self.setpoint_shift_max,
                self.setpoint_shift_min,
                self.on.group_addr_str(),
            )
        )
Пример #13
0
class Climate(Device):
    """Class for managing the climate."""
    def __init__(
        self,
        xknx: XKNX,
        name: str,
        group_address_temperature: GroupAddressesType | None = None,
        group_address_target_temperature: GroupAddressesType | None = None,
        group_address_target_temperature_state: GroupAddressesType
        | None = None,
        group_address_setpoint_shift: GroupAddressesType | None = None,
        group_address_setpoint_shift_state: GroupAddressesType | None = None,
        setpoint_shift_mode: SetpointShiftMode | None = None,
        setpoint_shift_max: float = DEFAULT_SETPOINT_SHIFT_MAX,
        setpoint_shift_min: float = DEFAULT_SETPOINT_SHIFT_MIN,
        temperature_step: float = DEFAULT_TEMPERATURE_STEP,
        group_address_on_off: GroupAddressesType | None = None,
        group_address_on_off_state: GroupAddressesType | None = None,
        on_off_invert: bool = False,
        min_temp: float | None = None,
        max_temp: float | None = None,
        mode: ClimateMode | None = None,
        create_temperature_sensors: bool = False,
        device_updated_cb: DeviceCallbackType | None = None,
    ):
        """Initialize Climate class."""
        super().__init__(xknx, name, device_updated_cb)

        self.min_temp = min_temp
        self.max_temp = max_temp
        self.setpoint_shift_min = setpoint_shift_min
        self.setpoint_shift_max = setpoint_shift_max
        self.temperature_step = temperature_step

        self.temperature = RemoteValueTemp(
            xknx,
            group_address_state=group_address_temperature,
            device_name=self.name,
            feature_name="Current temperature",
            after_update_cb=self.after_update,
        )

        self.target_temperature = RemoteValueTemp(
            xknx,
            group_address_target_temperature,
            group_address_target_temperature_state,
            device_name=self.name,
            feature_name="Target temperature",
            after_update_cb=self.after_update,
        )

        self._setpoint_shift = RemoteValueSetpointShift(
            xknx,
            group_address_setpoint_shift,
            group_address_setpoint_shift_state,
            device_name=self.name,
            after_update_cb=self.after_update,
            setpoint_shift_mode=setpoint_shift_mode,
            setpoint_shift_step=self.temperature_step,
        )

        self.supports_on_off = (group_address_on_off is not None
                                or group_address_on_off_state is not None)

        self.on = RemoteValueSwitch(  # pylint: disable=invalid-name
            xknx,
            group_address_on_off,
            group_address_on_off_state,
            device_name=self.name,
            after_update_cb=self.after_update,
            invert=on_off_invert,
        )

        self.mode = mode

        if create_temperature_sensors:
            self.create_temperature_sensors()

    def _iter_remote_values(self) -> Iterator[RemoteValue[Any, Any]]:
        """Iterate the devices RemoteValue classes."""
        yield self.temperature
        yield self.target_temperature
        yield self._setpoint_shift
        yield self.on

    def create_temperature_sensors(self) -> None:
        """Create temperature sensors."""
        for suffix, group_address, value_type in (
            (
                "temperature",
                self.temperature.group_address_state,
                "temperature",
            ),
            (
                "target temperature",
                self.target_temperature.group_address_state,
                "temperature",
            ),
        ):
            if group_address is not None:
                Sensor(
                    self.xknx,
                    name=self.name + " " + suffix,
                    group_address_state=group_address,
                    value_type=value_type,
                )

    @property
    def unique_id(self) -> str | None:
        """Return unique id for this device."""
        return f"{self.temperature.group_address_state}"

    def has_group_address(self, group_address: DeviceGroupAddress) -> bool:
        """Test if device has given group address."""
        if self.mode is not None and self.mode.has_group_address(
                group_address):
            return True
        return super().has_group_address(group_address)

    @property
    def is_on(self) -> bool:
        """Return power status."""
        # None will return False
        return bool(self.on.value)

    async def turn_on(self) -> None:
        """Set power status to on."""
        await self.on.on()

    async def turn_off(self) -> None:
        """Set power status to off."""
        await self.on.off()

    @property
    def initialized_for_setpoint_shift_calculations(self) -> bool:
        """Test if object is initialized for setpoint shift calculations."""
        if (self._setpoint_shift.initialized
                and self._setpoint_shift.value is not None
                and self.target_temperature.initialized
                and self.target_temperature.value is not None):
            return True
        return False

    async def set_target_temperature(self, target_temperature: float) -> None:
        """Send new target temperature or setpoint_shift to KNX bus."""
        if self.base_temperature is not None:
            # implies initialized_for_setpoint_shift_calculations
            temperature_delta = target_temperature - self.base_temperature
            await self.set_setpoint_shift(temperature_delta)
        else:
            validated_temp = self.validate_value(target_temperature,
                                                 self.min_temp, self.max_temp)
            await self.target_temperature.set(validated_temp)

    @property
    def base_temperature(self) -> float | None:
        """
        Return the base temperature when setpoint_shift is initialized.

        Base temperature is the default temperature (setpoint-shift=0) for the active climate mode.
        As this value is usually not available via KNX, we have to derive this from the current
        target temperature and the current set point shift.
        """
        # implies self.initialized_for_setpoint_shift_calculations in a mypy compatible way:
        if (self.target_temperature.value is not None
                and self._setpoint_shift.value is not None):
            return self.target_temperature.value - self._setpoint_shift.value
        return None

    @property
    def setpoint_shift(self) -> float | None:
        """Return current offset from base temperature in Kelvin."""
        return self._setpoint_shift.value

    def validate_value(self, value: float, min_value: float | None,
                       max_value: float | None) -> float:
        """Check boundaries of temperature and return valid temperature value."""
        if (min_value is not None) and (value < min_value):
            logger.warning("Min value exceeded at %s: %s", self.name, value)
            return min_value
        if (max_value is not None) and (value > max_value):
            logger.warning("Max value exceeded at %s: %s", self.name, value)
            return max_value
        return value

    async def set_setpoint_shift(self, offset: float) -> None:
        """Send new temperature offset to KNX bus."""
        validated_offset = self.validate_value(offset, self.setpoint_shift_min,
                                               self.setpoint_shift_max)
        base_temperature = self.base_temperature
        await self._setpoint_shift.set(validated_offset)
        # broadcast new target temperature and set internally
        if self.target_temperature.writable and base_temperature is not None:
            await self.target_temperature.set(base_temperature +
                                              validated_offset)

    @property
    def target_temperature_max(self) -> float | None:
        """Return the highest possible target temperature."""
        if self.max_temp is not None:
            return self.max_temp
        if self.base_temperature is not None:
            # implies initialized_for_setpoint_shift_calculations
            return self.base_temperature + self.setpoint_shift_max
        return None

    @property
    def target_temperature_min(self) -> float | None:
        """Return the lowest possible target temperature."""
        if self.min_temp is not None:
            return self.min_temp
        if self.base_temperature is not None:
            # implies initialized_for_setpoint_shift_calculations
            return self.base_temperature + self.setpoint_shift_min
        return None

    async def process_group_write(self, telegram: Telegram) -> None:
        """Process incoming and outgoing GROUP WRITE telegram."""
        for remote_value in self._iter_remote_values():
            await remote_value.process(telegram)
        if self.mode is not None:
            await self.mode.process_group_write(telegram)

    async def sync(self, wait_for_result: bool = False) -> None:
        """Read states of device from KNX bus."""
        await super().sync(wait_for_result=wait_for_result)
        if self.mode is not None:
            await self.mode.sync(wait_for_result=wait_for_result)

    def __str__(self) -> str:
        """Return object as readable string."""
        return ('<Climate name="{}" '
                "temperature={} "
                "target_temperature={} "
                'temperature_step="{}" '
                "setpoint_shift={} "
                'setpoint_shift_max="{}" '
                'setpoint_shift_min="{}" '
                "group_address_on_off={} "
                "/>".format(
                    self.name,
                    self.temperature.group_addr_str(),
                    self.target_temperature.group_addr_str(),
                    self.temperature_step,
                    self._setpoint_shift.group_addr_str(),
                    self.setpoint_shift_max,
                    self.setpoint_shift_min,
                    self.on.group_addr_str(),
                ))
Пример #14
0
class Weather(Device):
    """Class for managing a weather device."""

    # pylint: disable=too-many-locals
    def __init__(
        self,
        xknx: "XKNX",
        name: str,
        group_address_temperature: Optional["GroupAddressableType"] = None,
        group_address_brightness_south: Optional[
            "GroupAddressableType"] = None,
        group_address_brightness_north: Optional[
            "GroupAddressableType"] = None,
        group_address_brightness_west: Optional["GroupAddressableType"] = None,
        group_address_brightness_east: Optional["GroupAddressableType"] = None,
        group_address_wind_speed: Optional["GroupAddressableType"] = None,
        group_address_wind_bearing: Optional["GroupAddressableType"] = None,
        group_address_rain_alarm: Optional["GroupAddressableType"] = None,
        group_address_frost_alarm: Optional["GroupAddressableType"] = None,
        group_address_wind_alarm: Optional["GroupAddressableType"] = None,
        group_address_day_night: Optional["GroupAddressableType"] = None,
        group_address_air_pressure: Optional["GroupAddressableType"] = None,
        group_address_humidity: Optional["GroupAddressableType"] = None,
        create_sensors: bool = False,
        sync_state: bool = True,
        device_updated_cb: Optional[DeviceCallbackType] = None,
    ) -> None:
        """Initialize Weather class."""
        # pylint: disable=too-many-arguments
        super().__init__(xknx, name, device_updated_cb)

        self._temperature = RemoteValueSensor(
            xknx,
            group_address_state=group_address_temperature,
            sync_state=sync_state,
            value_type="temperature",
            device_name=self.name,
            feature_name="Temperature",
            after_update_cb=self.after_update,
        )

        self._brightness_south = RemoteValueSensor(
            xknx,
            group_address_state=group_address_brightness_south,
            sync_state=sync_state,
            value_type="illuminance",
            device_name=self.name,
            feature_name="Brightness south",
            after_update_cb=self.after_update,
        )

        self._brightness_north = RemoteValueSensor(
            xknx,
            group_address_state=group_address_brightness_north,
            sync_state=sync_state,
            value_type="illuminance",
            device_name=self.name,
            feature_name="Brightness north",
            after_update_cb=self.after_update,
        )

        self._brightness_west = RemoteValueSensor(
            xknx,
            group_address_state=group_address_brightness_west,
            sync_state=sync_state,
            value_type="illuminance",
            device_name=self.name,
            feature_name="Brightness west",
            after_update_cb=self.after_update,
        )

        self._brightness_east = RemoteValueSensor(
            xknx,
            group_address_state=group_address_brightness_east,
            sync_state=sync_state,
            value_type="illuminance",
            device_name=self.name,
            feature_name="Brightness east",
            after_update_cb=self.after_update,
        )

        self._wind_speed = RemoteValueSensor(
            xknx,
            group_address_state=group_address_wind_speed,
            sync_state=sync_state,
            value_type="wind_speed_ms",
            device_name=self.name,
            feature_name="Wind speed",
            after_update_cb=self.after_update,
        )

        self._wind_bearing = RemoteValueSensor(
            xknx,
            group_address_state=group_address_wind_bearing,
            sync_state=sync_state,
            value_type="angle",
            device_name=self.name,
            feature_name="Wind bearing",
            after_update_cb=self.after_update,
        )

        self._rain_alarm = RemoteValueSwitch(
            xknx,
            group_address_state=group_address_rain_alarm,
            device_name=self.name,
            feature_name="Rain alarm",
            after_update_cb=self.after_update,
        )

        self._frost_alarm = RemoteValueSwitch(
            xknx,
            group_address_state=group_address_frost_alarm,
            device_name=self.name,
            feature_name="Frost alarm",
            after_update_cb=self.after_update,
        )

        self._wind_alarm = RemoteValueSwitch(
            xknx,
            group_address_state=group_address_wind_alarm,
            device_name=self.name,
            feature_name="Wind alarm",
            after_update_cb=self.after_update,
        )

        self._day_night = RemoteValueSwitch(
            xknx,
            group_address_state=group_address_day_night,
            device_name=self.name,
            feature_name="Day/Night",
            after_update_cb=self.after_update,
        )

        self._air_pressure = RemoteValueSensor(
            xknx,
            group_address_state=group_address_air_pressure,
            sync_state=sync_state,
            value_type="pressure_2byte",
            device_name=self.name,
            feature_name="Air pressure",
            after_update_cb=self.after_update,
        )

        self._humidity = RemoteValueSensor(
            xknx,
            group_address_state=group_address_humidity,
            sync_state=sync_state,
            value_type="humidity",
            device_name=self.name,
            feature_name="Humidity",
            after_update_cb=self.after_update,
        )

        if create_sensors:
            self.create_sensors()

    def _iter_remote_values(self) -> Iterator[RemoteValue[Any]]:
        """Iterate the devices remote values."""
        yield self._temperature
        yield self._brightness_south
        yield self._brightness_north
        yield self._brightness_east
        yield self._brightness_west
        yield self._wind_speed
        yield self._wind_bearing
        yield self._rain_alarm
        yield self._wind_alarm
        yield self._frost_alarm
        yield self._day_night
        yield self._air_pressure
        yield self._humidity

    async def process_group_write(self, telegram: "Telegram") -> None:
        """Process incoming and outgoing GROUP WRITE telegram."""
        for remote_value in self._iter_remote_values():
            await remote_value.process(telegram)

    @property
    def temperature(self) -> Optional[float]:
        """Return current temperature."""
        return self._temperature.value  # type: ignore

    @property
    def brightness_south(self) -> float:
        """Return brightness south."""
        return (  # type: ignore
            0.0 if self._brightness_south.value is None else
            self._brightness_south.value)

    @property
    def brightness_north(self) -> float:
        """Return brightness north."""
        return (  # type: ignore
            0.0 if self._brightness_north.value is None else
            self._brightness_north.value)

    @property
    def brightness_east(self) -> float:
        """Return brightness east."""
        return (  # type: ignore
            0.0 if self._brightness_east.value is None else
            self._brightness_east.value)

    @property
    def brightness_west(self) -> float:
        """Return brightness west."""
        return (  # type: ignore
            0.0 if self._brightness_west.value is None else
            self._brightness_west.value)

    @property
    def wind_speed(self) -> Optional[float]:
        """Return wind speed in m/s."""
        return self._wind_speed.value  # type: ignore

    @property
    def wind_bearing(self) -> Optional[int]:
        """Return wind bearing in °."""
        return self._wind_bearing.value  # type: ignore

    @property
    def rain_alarm(self) -> Optional[bool]:
        """Return True if rain alarm False if not."""
        return self._rain_alarm.value  # type: ignore

    @property
    def wind_alarm(self) -> Optional[bool]:
        """Return True if wind alarm False if not."""
        return self._wind_alarm.value  # type: ignore

    @property
    def frost_alarm(self) -> Optional[bool]:
        """Return True if frost alarm False if not."""
        return self._frost_alarm.value  # type: ignore

    @property
    def day_night(self) -> Optional[bool]:
        """Return day or night."""
        return self._day_night.value  # type: ignore

    @property
    def air_pressure(self) -> Optional[float]:
        """Return pressure in Pa."""
        return self._air_pressure.value  # type: ignore

    @property
    def humidity(self) -> Optional[float]:
        """Return humidity in %."""
        return self._humidity.value  # type: ignore

    @property
    def max_brightness(self) -> float:
        """Return highest illuminance from all sensors."""
        return max(
            self.brightness_west,
            self.brightness_south,
            self.brightness_north,
            self.brightness_east,
        )

    def create_sensors(self) -> None:
        """Expose sensors to xknx."""
        for suffix, group_address in (
            ("_rain_alarm", self._rain_alarm.group_address_state),
            ("_wind_alarm", self._wind_alarm.group_address_state),
            ("_frost_alarm", self._frost_alarm.group_address_state),
            ("_day_night", self._day_night.group_address_state),
        ):
            if group_address is not None:
                BinarySensor(
                    self.xknx,
                    name=self.name + suffix,
                    group_address_state=group_address,
                )

        for suffix, group_address, value_type in (
            (
                "_temperature",
                self._temperature.group_address_state,
                "temperature",
            ),
            (
                "_brightness_south",
                self._brightness_south.group_address_state,
                "illuminance",
            ),
            (
                "_brightness_north",
                self._brightness_north.group_address_state,
                "illuminance",
            ),
            (
                "_brightness_west",
                self._brightness_west.group_address_state,
                "illuminance",
            ),
            (
                "_brightness_east",
                self._brightness_east.group_address_state,
                "illuminance",
            ),
            (
                "_wind_speed",
                self._wind_speed.group_address_state,
                "wind_speed_ms",
            ),
            (
                "_wind_bearing",
                self._wind_bearing.group_address_state,
                "angle",
            ),
            (
                "_air_pressure",
                self._air_pressure.group_address_state,
                "pressure",
            ),
            (
                "_humidity",
                self._humidity.group_address_state,
                "humidity",
            ),
        ):
            if group_address is not None:
                Sensor(
                    self.xknx,
                    name=self.name + suffix,
                    group_address_state=group_address,
                    value_type=value_type,
                )

    # pylint: disable=too-many-return-statements
    def ha_current_state(
        self, current_date: date = date.today()) -> WeatherCondition:
        """Return the current state for home assistant."""
        def _get_season(now: date) -> Season:
            """Return winter or summer."""
            if isinstance(now, datetime):
                now = now.date()
            now = now.replace(year=YEAR)
            return next(season for season, (start, end) in SEASONS
                        if start <= now <= end)

        if self.wind_alarm and self.rain_alarm:
            return WeatherCondition.lightning_rainy

        if self.frost_alarm and self.rain_alarm:
            return WeatherCondition.snowy_rainy

        if self.rain_alarm:
            return WeatherCondition.rainy

        if self.wind_alarm:
            return WeatherCondition.windy

        current_season: Season = _get_season(current_date)
        _season: Season
        function: Callable[[float], bool]
        result: WeatherCondition
        for _season, function, result in ILLUMINANCE_MAPPING:
            if _season == current_season and function(self.max_brightness):
                return result

        if self.day_night is False:
            return WeatherCondition.clear_night

        return WeatherCondition.exceptional

    @classmethod
    def from_config(cls, xknx: "XKNX", name: str, config: Any) -> "Weather":
        """Initialize object from configuration structure."""
        group_address_temperature = config.get("group_address_temperature")
        group_address_brightness_south = config.get(
            "group_address_brightness_south")
        group_address_brightness_north = config.get(
            "group_address_brightness_north")
        group_address_brightness_west = config.get(
            "group_address_brightness_west")
        group_address_brightness_east = config.get(
            "group_address_brightness_east")
        group_address_wind_speed = config.get("group_address_wind_speed")
        group_address_wind_bearing = config.get("group_address_wind_bearing")
        group_address_rain_alarm = config.get("group_address_rain_alarm")
        group_address_frost_alarm = config.get("group_address_frost_alarm")
        group_address_wind_alarm = config.get("group_address_wind_alarm")
        group_address_day_night = config.get("group_address_day_night")
        group_address_air_pressure = config.get("group_address_air_pressure")
        group_address_humidity = config.get("group_address_humidity")
        create_sensors = config.get("create_sensors", False)
        sync_state = config.get("sync_state", True)

        return cls(
            xknx,
            name,
            group_address_temperature=group_address_temperature,
            group_address_brightness_south=group_address_brightness_south,
            group_address_brightness_north=group_address_brightness_north,
            group_address_brightness_west=group_address_brightness_west,
            group_address_brightness_east=group_address_brightness_east,
            group_address_wind_speed=group_address_wind_speed,
            group_address_wind_bearing=group_address_wind_bearing,
            group_address_rain_alarm=group_address_rain_alarm,
            group_address_frost_alarm=group_address_frost_alarm,
            group_address_wind_alarm=group_address_wind_alarm,
            group_address_day_night=group_address_day_night,
            group_address_air_pressure=group_address_air_pressure,
            group_address_humidity=group_address_humidity,
            create_sensors=create_sensors,
            sync_state=sync_state,
        )

    def __str__(self) -> str:
        """Return object as readable string."""
        return (
            '<Weather name="{}" '
            'temperature="{}" brightness_south="{}" brightness_north="{}" brightness_west="{}" '
            'brightness_east="{}" wind_speed="{}" wind_bearing="{}" rain_alarm="{}" '
            'wind_alarm="{}" frost_alarm="{}" day_night="{}" '
            'air_pressure="{}" humidity="{}" />'.format(
                self.name,
                self._temperature.group_addr_str(),
                self._brightness_south.group_addr_str(),
                self._brightness_north.group_addr_str(),
                self._brightness_west.group_addr_str(),
                self._brightness_east.group_addr_str(),
                self._wind_speed.group_addr_str(),
                self._wind_bearing.group_addr_str(),
                self._rain_alarm.group_addr_str(),
                self._wind_alarm.group_addr_str(),
                self._frost_alarm.group_addr_str(),
                self._day_night.group_addr_str(),
                self._air_pressure.group_addr_str(),
                self._humidity.group_addr_str(),
            ))
Пример #15
0
class Light(Device):
    """Class for managing a light."""

    # pylint: disable=too-many-locals
    DEFAULT_MIN_KELVIN = 2700  # 370 mireds
    DEFAULT_MAX_KELVIN = 6000  # 166 mireds

    def __init__(
        self,
        xknx: "XKNX",
        name: str,
        group_address_switch: Optional["GroupAddressableType"] = None,
        group_address_switch_state: Optional["GroupAddressableType"] = None,
        group_address_brightness: Optional["GroupAddressableType"] = None,
        group_address_brightness_state: Optional["GroupAddressableType"] = None,
        group_address_color: Optional["GroupAddressableType"] = None,
        group_address_color_state: Optional["GroupAddressableType"] = None,
        group_address_rgbw: Optional["GroupAddressableType"] = None,
        group_address_rgbw_state: Optional["GroupAddressableType"] = None,
        group_address_tunable_white: Optional["GroupAddressableType"] = None,
        group_address_tunable_white_state: Optional["GroupAddressableType"] = None,
        group_address_color_temperature: Optional["GroupAddressableType"] = None,
        group_address_color_temperature_state: Optional["GroupAddressableType"] = None,
        group_address_switch_red: Optional["GroupAddressableType"] = None,
        group_address_switch_red_state: Optional["GroupAddressableType"] = None,
        group_address_brightness_red: Optional["GroupAddressableType"] = None,
        group_address_brightness_red_state: Optional["GroupAddressableType"] = None,
        group_address_switch_green: Optional["GroupAddressableType"] = None,
        group_address_switch_green_state: Optional["GroupAddressableType"] = None,
        group_address_brightness_green: Optional["GroupAddressableType"] = None,
        group_address_brightness_green_state: Optional["GroupAddressableType"] = None,
        group_address_switch_blue: Optional["GroupAddressableType"] = None,
        group_address_switch_blue_state: Optional["GroupAddressableType"] = None,
        group_address_brightness_blue: Optional["GroupAddressableType"] = None,
        group_address_brightness_blue_state: Optional["GroupAddressableType"] = None,
        group_address_switch_white: Optional["GroupAddressableType"] = None,
        group_address_switch_white_state: Optional["GroupAddressableType"] = None,
        group_address_brightness_white: Optional["GroupAddressableType"] = None,
        group_address_brightness_white_state: Optional["GroupAddressableType"] = None,
        min_kelvin: Optional[int] = None,
        max_kelvin: Optional[int] = None,
        device_updated_cb: Optional[DeviceCallbackType] = None,
    ):
        """Initialize Light class."""
        # pylint: disable=too-many-arguments
        super().__init__(xknx, name, device_updated_cb)

        self.switch = RemoteValueSwitch(
            xknx,
            group_address_switch,
            group_address_switch_state,
            device_name=self.name,
            feature_name="State",
            after_update_cb=self.after_update,
        )

        self.brightness = RemoteValueScaling(
            xknx,
            group_address_brightness,
            group_address_brightness_state,
            device_name=self.name,
            feature_name="Brightness",
            after_update_cb=self.after_update,
            range_from=0,
            range_to=255,
        )

        self.color = RemoteValueColorRGB(
            xknx,
            group_address_color,
            group_address_color_state,
            device_name=self.name,
            after_update_cb=self.after_update,
        )

        self.rgbw = RemoteValueColorRGBW(
            xknx,
            group_address_rgbw,
            group_address_rgbw_state,
            device_name=self.name,
            after_update_cb=self.after_update,
        )

        self.tunable_white = RemoteValueScaling(
            xknx,
            group_address_tunable_white,
            group_address_tunable_white_state,
            device_name=self.name,
            feature_name="Tunable white",
            after_update_cb=self.after_update,
            range_from=0,
            range_to=255,
        )

        self.color_temperature = RemoteValueDpt2ByteUnsigned(
            xknx,
            group_address_color_temperature,
            group_address_color_temperature_state,
            device_name=self.name,
            feature_name="Color temperature",
            after_update_cb=self.after_update,
        )

        self.red = _SwitchAndBrightness(
            xknx,
            self.name,
            "red",
            group_address_switch_red,
            group_address_switch_red_state,
            group_address_brightness_red,
            group_address_brightness_red_state,
            self.after_update,
        )

        self.green = _SwitchAndBrightness(
            xknx,
            self.name,
            "green",
            group_address_switch_green,
            group_address_switch_green_state,
            group_address_brightness_green,
            group_address_brightness_green_state,
            self.after_update,
        )

        self.blue = _SwitchAndBrightness(
            xknx,
            self.name,
            "blue",
            group_address_switch_blue,
            group_address_switch_blue_state,
            group_address_brightness_blue,
            group_address_brightness_blue_state,
            self.after_update,
        )

        self.white = _SwitchAndBrightness(
            xknx,
            self.name,
            "white",
            group_address_switch_white,
            group_address_switch_white_state,
            group_address_brightness_white,
            group_address_brightness_white_state,
            self.after_update,
        )

        self.min_kelvin = min_kelvin
        self.max_kelvin = max_kelvin

    def _iter_remote_values(self) -> Iterator["RemoteValue"]:
        """Iterate the devices RemoteValue classes."""
        yield from (
            self.switch,
            self.brightness,
            self.color,
            self.rgbw,
            self.tunable_white,
            self.color_temperature,
        )
        for color in (self.red, self.green, self.blue, self.white):
            yield color.switch
            yield color.brightness

    @property
    def supports_brightness(self) -> bool:
        """Return if light supports brightness."""
        return self.brightness.initialized

    @property
    def supports_color(self) -> bool:
        """Return if light supports color."""
        return self.color.initialized or all(
            [c.brightness.initialized for c in (self.red, self.green, self.blue)]
        )

    @property
    def supports_rgbw(self) -> bool:
        """Return if light supports RGBW."""
        return self.rgbw.initialized or all(
            [
                c.brightness.initialized
                for c in (self.red, self.green, self.blue, self.white)
            ]
        )

    @property
    def supports_tunable_white(self) -> bool:
        """Return if light supports tunable white / relative color temperature."""
        return self.tunable_white.initialized

    @property
    def supports_color_temperature(self) -> bool:
        """Return if light supports absolute color temperature."""
        return self.color_temperature.initialized

    @classmethod
    def read_color_from_config(
        cls, color: str, config: Any
    ) -> Tuple[
        Optional["GroupAddressableType"],
        Optional["GroupAddressableType"],
        Optional["GroupAddressableType"],
        Optional["GroupAddressableType"],
    ]:
        """Load color configuration from configuration structure."""
        if "individual_colors" in config and color in config["individual_colors"]:
            sub_config = config["individual_colors"][color]
            return (
                sub_config.get("group_address_switch"),
                sub_config.get("group_address_switch_state"),
                sub_config.get("group_address_brightness"),
                sub_config.get("group_address_brightness_state"),
            )
        return None, None, None, None

    @classmethod
    def from_config(cls, xknx: "XKNX", name: str, config: Any) -> "Light":
        """Initialize object from configuration structure."""
        group_address_switch = config.get("group_address_switch")
        group_address_switch_state = config.get("group_address_switch_state")
        group_address_brightness = config.get("group_address_brightness")
        group_address_brightness_state = config.get("group_address_brightness_state")
        group_address_color = config.get("group_address_color")
        group_address_color_state = config.get("group_address_color_state")
        group_address_rgbw = config.get("group_address_rgbw")
        group_address_rgbw_state = config.get("group_address_rgbw_state")
        group_address_tunable_white = config.get("group_address_tunable_white")
        group_address_tunable_white_state = config.get(
            "group_address_tunable_white_state"
        )
        group_address_color_temperature = config.get("group_address_color_temperature")
        group_address_color_temperature_state = config.get(
            "group_address_color_temperature_state"
        )
        min_kelvin = config.get("min_kelvin", Light.DEFAULT_MIN_KELVIN)
        max_kelvin = config.get("max_kelvin", Light.DEFAULT_MAX_KELVIN)

        (
            red_switch,
            red_switch_state,
            red_brightness,
            red_brightness_state,
        ) = cls.read_color_from_config("red", config)
        (
            green_switch,
            green_switch_state,
            green_brightness,
            green_brightness_state,
        ) = cls.read_color_from_config("green", config)
        (
            blue_switch,
            blue_switch_state,
            blue_brightness,
            blue_brightness_state,
        ) = cls.read_color_from_config("blue", config)
        (
            white_switch,
            white_switch_state,
            white_brightness,
            white_brightness_state,
        ) = cls.read_color_from_config("white", config)

        return cls(
            xknx,
            name,
            group_address_switch=group_address_switch,
            group_address_switch_state=group_address_switch_state,
            group_address_brightness=group_address_brightness,
            group_address_brightness_state=group_address_brightness_state,
            group_address_color=group_address_color,
            group_address_color_state=group_address_color_state,
            group_address_rgbw=group_address_rgbw,
            group_address_rgbw_state=group_address_rgbw_state,
            group_address_tunable_white=group_address_tunable_white,
            group_address_tunable_white_state=group_address_tunable_white_state,
            group_address_color_temperature=group_address_color_temperature,
            group_address_color_temperature_state=group_address_color_temperature_state,
            group_address_switch_red=red_switch,
            group_address_switch_red_state=red_switch_state,
            group_address_brightness_red=red_brightness,
            group_address_brightness_red_state=red_brightness_state,
            group_address_switch_green=green_switch,
            group_address_switch_green_state=green_switch_state,
            group_address_brightness_green=green_brightness,
            group_address_brightness_green_state=green_brightness_state,
            group_address_switch_blue=blue_switch,
            group_address_switch_blue_state=blue_switch_state,
            group_address_brightness_blue=blue_brightness,
            group_address_brightness_blue_state=blue_brightness_state,
            group_address_switch_white=white_switch,
            group_address_switch_white_state=white_switch_state,
            group_address_brightness_white=white_brightness,
            group_address_brightness_white_state=white_brightness_state,
            min_kelvin=min_kelvin,
            max_kelvin=max_kelvin,
        )

    def __str__(self) -> str:
        """Return object as readable string."""
        str_brightness = (
            ""
            if not self.supports_brightness
            else f' brightness="{self.brightness.group_addr_str()}"'
        )

        str_color = (
            "" if not self.supports_color else f' color="{self.color.group_addr_str()}"'
        )

        str_rgbw = (
            "" if not self.supports_rgbw else f' rgbw="{self.rgbw.group_addr_str()}"'
        )

        str_tunable_white = (
            ""
            if not self.supports_tunable_white
            else f' tunable white="{self.tunable_white.group_addr_str()}"'
        )

        str_color_temperature = (
            ""
            if not self.supports_color_temperature
            else ' color temperature="{}"'.format(
                self.color_temperature.group_addr_str()
            )
        )

        str_red_state = (
            ""
            if not self.red.switch.initialized
            else f' red_state="{self.red.switch.group_addr_str()}"'
        )
        str_red_brightness = (
            ""
            if not self.red.brightness.initialized
            else f' red_brightness="{self.red.brightness.group_addr_str()}"'
        )

        str_green_state = (
            ""
            if not self.green.switch.initialized
            else f' green_state="{self.green.switch.group_addr_str()}"'
        )
        str_green_brightness = (
            ""
            if not self.green.brightness.initialized
            else f' green_brightness="{self.green.brightness.group_addr_str()}"'
        )

        str_blue_state = (
            ""
            if not self.blue.switch.initialized
            else f' blue_state="{self.blue.switch.group_addr_str()}"'
        )
        str_blue_brightness = (
            ""
            if not self.blue.brightness.initialized
            else f' blue_brightness="{self.blue.brightness.group_addr_str()}"'
        )

        str_white_state = (
            ""
            if not self.white.switch.initialized
            else f' white_state="{self.white.switch.group_addr_str()}"'
        )
        str_white_brightness = (
            ""
            if not self.white.brightness.initialized
            else f' white_brightness="{self.white.brightness.group_addr_str()}"'
        )

        return '<Light name="{}" ' 'switch="{}"{}{}{}{}{}{}{}{}{}{}{}{}{} />'.format(
            self.name,
            self.switch.group_addr_str(),
            str_brightness,
            str_color,
            str_rgbw,
            str_tunable_white,
            str_color_temperature,
            str_red_state,
            str_red_brightness,
            str_green_state,
            str_green_brightness,
            str_blue_state,
            str_blue_brightness,
            str_white_state,
            str_white_brightness,
        )

    @property
    def state(self) -> Optional[bool]:
        """Return the current switch state of the device."""
        if self.switch.value is not None:
            return self.switch.value  # type: ignore
        if any(
            [
                c.switch.value is not None
                for c in (self.red, self.green, self.blue, self.white)
            ]
        ):
            return any(
                [c.switch.value for c in (self.red, self.green, self.blue, self.white)]
            )
        return None

    async def set_on(self) -> None:
        """Switch light on."""
        if self.switch.initialized:
            await self.switch.on()
        for color in (self.red, self.green, self.blue, self.white):
            await color.set_on()

    async def set_off(self) -> None:
        """Switch light off."""
        if self.switch.initialized:
            await self.switch.off()
        for color in (self.red, self.green, self.blue, self.white):
            await color.set_off()

    @property
    def current_brightness(self) -> Optional[int]:
        """Return current brightness of light."""
        return self.brightness.value  # type: ignore

    async def set_brightness(self, brightness: int) -> None:
        """Set brightness of light."""
        if not self.supports_brightness:
            logger.warning("Dimming not supported for device %s", self.get_name())
            return
        await self.brightness.set(brightness)

    @property
    def current_color(self) -> Tuple[Optional[List[int]], Optional[int]]:
        """
        Return current color of light.

        If the device supports RGBW, get the current RGB+White values instead.
        """
        if self.supports_rgbw:
            if self.rgbw.initialized:
                if not self.rgbw.value:
                    return None, None
                return self.rgbw.value[:3], self.rgbw.value[3]
        if self.color.initialized:
            return self.color.value, None
        # individual RGB addresses - white will return None when it is not initialized
        colors = [
            self.red.brightness.value,
            self.green.brightness.value,
            self.blue.brightness.value,
        ]
        if None in colors:
            return None, self.white.brightness.value
        return colors, self.white.brightness.value

    async def set_color(self, color: List[int], white: Optional[int] = None) -> None:
        """
        Set color of a light device.

        If also the white value is given and the device supports RGBW,
        set all four values.
        """
        if white is not None:
            if self.supports_rgbw:
                if self.rgbw.initialized:
                    await self.rgbw.set(list(color) + [white])
                    return
                if all(
                    [
                        c.brightness.initialized
                        for c in (self.red, self.green, self.blue, self.white)
                    ]
                ):
                    await self.red.brightness.set(color[0])
                    await self.green.brightness.set(color[1])
                    await self.blue.brightness.set(color[2])
                    await self.white.brightness.set(white)
                    return
            logger.warning("RGBW not supported for device %s", self.get_name())
        else:
            if self.supports_color:
                if self.color.initialized:
                    await self.color.set(color)
                    return
                if all(
                    [
                        c.brightness.initialized
                        for c in (self.red, self.green, self.blue)
                    ]
                ):
                    await self.red.brightness.set(color[0])
                    await self.green.brightness.set(color[1])
                    await self.blue.brightness.set(color[2])
                    return
            logger.warning("Colors not supported for device %s", self.get_name())

    @property
    def current_tunable_white(self) -> Optional[int]:
        """Return current relative color temperature of light."""
        return self.tunable_white.value  # type: ignore

    async def set_tunable_white(self, tunable_white: int) -> None:
        """Set relative color temperature of light."""
        if not self.supports_tunable_white:
            logger.warning("Tunable white not supported for device %s", self.get_name())
            return
        await self.tunable_white.set(tunable_white)

    @property
    def current_color_temperature(self) -> Optional[int]:
        """Return current absolute color temperature of light."""
        return self.color_temperature.value  # type: ignore

    async def set_color_temperature(self, color_temperature: int) -> None:
        """Set absolute color temperature of light."""
        if not self.supports_color_temperature:
            logger.warning(
                "Absolute Color Temperature not supported for device %s",
                self.get_name(),
            )
            return
        await self.color_temperature.set(color_temperature)

    async def do(self, action: str) -> None:
        """Execute 'do' commands."""
        if action == "on":
            await self.set_on()
        elif action == "off":
            await self.set_off()
        elif action.startswith("brightness:"):
            await self.set_brightness(int(action[11:]))
        elif action.startswith("tunable_white:"):
            await self.set_tunable_white(int(action[14:]))
        elif action.startswith("color_temperature:"):
            await self.set_color_temperature(int(action[18:]))
        else:
            logger.warning(
                "Could not understand action %s for device %s", action, self.get_name()
            )

    async def process_group_write(self, telegram: "Telegram") -> None:
        """Process incoming and outgoing GROUP WRITE telegram."""
        for remote_value in self._iter_remote_values():
            await remote_value.process(telegram)
Пример #16
0
class Climate(Device):
    """Class for managing the climate."""

    # pylint: disable=too-many-instance-attributes,invalid-name
    def __init__(self,
                 xknx,
                 name,
                 group_address_temperature=None,
                 group_address_target_temperature=None,
                 group_address_target_temperature_state=None,
                 group_address_setpoint_shift=None,
                 group_address_setpoint_shift_state=None,
                 setpoint_shift_mode=DEFAULT_SETPOINT_SHIFT_MODE,
                 setpoint_shift_step=DEFAULT_SETPOINT_SHIFT_STEP,
                 setpoint_shift_max=DEFAULT_SETPOINT_SHIFT_MAX,
                 setpoint_shift_min=DEFAULT_SETPOINT_SHIFT_MIN,
                 group_address_on_off=None,
                 group_address_on_off_state=None,
                 on_off_invert=False,
                 min_temp=None,
                 max_temp=None,
                 mode=None,
                 device_updated_cb=None):
        """Initialize Climate class."""
        # pylint: disable=too-many-arguments, too-many-locals, too-many-branches, too-many-statements
        super().__init__(xknx, name, device_updated_cb)
        if isinstance(group_address_on_off, (str, int)):
            group_address_on_off = GroupAddress(group_address_on_off)
        if isinstance(group_address_on_off_state, (str, int)):
            group_address_on_off_state = GroupAddress(
                group_address_on_off_state)

        self.group_address_on_off = group_address_on_off
        self.group_address_on_off_state = group_address_on_off_state

        self.min_temp = min_temp
        self.max_temp = max_temp
        self.setpoint_shift_step = setpoint_shift_step
        self.setpoint_shift_min = setpoint_shift_min
        self.setpoint_shift_max = setpoint_shift_max

        self.temperature = RemoteValueTemp(
            xknx,
            group_address_state=group_address_temperature,
            device_name=self.name,
            after_update_cb=self.after_update)

        self.target_temperature = RemoteValueTemp(
            xknx,
            group_address_target_temperature,
            group_address_target_temperature_state,
            device_name=self.name,
            after_update_cb=self.after_update)

        if setpoint_shift_mode == SetpointShiftMode.DPT9002:
            self._setpoint_shift = RemoteValueTemp(
                xknx,
                group_address_setpoint_shift,
                group_address_setpoint_shift_state,
                device_name=self.name,
                after_update_cb=self.after_update)
        else:
            self._setpoint_shift = RemoteValueSetpointShift(
                xknx,
                group_address_setpoint_shift,
                group_address_setpoint_shift_state,
                device_name=self.name,
                after_update_cb=self.after_update,
                setpoint_shift_step=setpoint_shift_step)

        self.supports_on_off = \
            group_address_on_off is not None or \
            group_address_on_off_state is not None

        self.on = RemoteValueSwitch(xknx,
                                    group_address_on_off,
                                    group_address_on_off_state,
                                    device_name=self.name,
                                    after_update_cb=self.after_update,
                                    invert=on_off_invert)

        self.mode = mode

    @classmethod
    def from_config(cls, xknx, name, config):
        """Initialize object from configuration structure."""
        # pylint: disable=too-many-locals
        group_address_temperature = \
            config.get('group_address_temperature')
        group_address_target_temperature = \
            config.get('group_address_target_temperature')
        group_address_target_temperature_state = \
            config.get('group_address_target_temperature_state')
        group_address_setpoint_shift = \
            config.get('group_address_setpoint_shift')
        group_address_setpoint_shift_state = \
            config.get('group_address_setpoint_shift_state')
        setpoint_shift_mode = \
            config.get('setpoint_shift_mode', DEFAULT_SETPOINT_SHIFT_MODE)
        setpoint_shift_step = \
            config.get('setpoint_shift_step', DEFAULT_SETPOINT_SHIFT_STEP)
        setpoint_shift_max = \
            config.get('setpoint_shift_max', DEFAULT_SETPOINT_SHIFT_MAX)
        setpoint_shift_min = \
            config.get('setpoint_shift_min', DEFAULT_SETPOINT_SHIFT_MIN)
        group_address_on_off = \
            config.get('group_address_on_off')
        group_address_on_off_state = \
            config.get('group_address_on_off_state')
        on_off_invert = \
            config.get('on_off_invert', False)
        min_temp = config.get('min_temp')
        max_temp = config.get('max_temp')

        climate_mode = None
        if "mode" in config:
            climate_mode = ClimateMode.from_config(xknx=xknx,
                                                   name=None,
                                                   config=config['mode'])

        return cls(
            xknx,
            name,
            group_address_temperature=group_address_temperature,
            group_address_target_temperature=group_address_target_temperature,
            group_address_target_temperature_state=
            group_address_target_temperature_state,
            group_address_setpoint_shift=group_address_setpoint_shift,
            group_address_setpoint_shift_state=
            group_address_setpoint_shift_state,
            setpoint_shift_mode=setpoint_shift_mode,
            setpoint_shift_step=setpoint_shift_step,
            setpoint_shift_max=setpoint_shift_max,
            setpoint_shift_min=setpoint_shift_min,
            group_address_on_off=group_address_on_off,
            group_address_on_off_state=group_address_on_off_state,
            on_off_invert=on_off_invert,
            min_temp=min_temp,
            max_temp=max_temp,
            mode=climate_mode)

    def has_group_address(self, group_address):
        """Test if device has given group address."""
        if self.mode is not None and self.mode.has_group_address(
                group_address):
            return True
        return self.temperature.has_group_address(group_address) or \
            self.target_temperature.has_group_address(group_address) or \
            self._setpoint_shift.has_group_address(group_address) or \
            self.on.has_group_address(group_address)

    @property
    def is_on(self):
        """Return power status."""
        # None will return False
        return bool(self.on.value)

    async def turn_on(self):
        """Set power status to on."""
        await self.on.on()

    async def turn_off(self):
        """Set power status to off."""
        await self.on.off()

    @property
    def initialized_for_setpoint_shift_calculations(self):
        """Test if object is initialized for setpoint shift calculations."""
        if not self._setpoint_shift.initialized:
            return False
        if self._setpoint_shift.value is None:
            return False
        if not self.target_temperature.initialized:
            return False
        if self.target_temperature.value is None:
            return False
        return True

    @property
    def temperature_step(self):
        """Return smallest possible temperature step."""
        if self._setpoint_shift.initialized:
            return self.setpoint_shift_step
        return DEFAULT_TEMPERATURE_STEP

    async def set_target_temperature(self, target_temperature):
        """Send new target temperature or setpoint_shift to KNX bus."""
        if self.initialized_for_setpoint_shift_calculations:
            temperature_delta = target_temperature - self.base_temperature
            await self.set_setpoint_shift(temperature_delta)
        else:
            validated_temp = self.validate_value(target_temperature,
                                                 self.min_temp, self.max_temp)
            await self.target_temperature.set(validated_temp)

    @property
    def base_temperature(self):
        """
        Return the base temperature.

        Base temperature is the default temperature (setpoint-shift=0) for the active climate mode.
        As this value is usually not available via KNX, we have to derive this from the current
        target temperature and the current set point shift.
        """
        if self.initialized_for_setpoint_shift_calculations:
            return self.target_temperature.value - self.setpoint_shift
        return None

    @property
    def setpoint_shift(self):
        """Return current offset from base temperature in Kelvin."""
        return self._setpoint_shift.value

    def validate_value(self, value, min_value, max_value):
        """Check boundaries of temperature and return valid temperature value."""
        if (min_value is not None) and (value < min_value):
            self.xknx.logger.warning("min value exceeded at %s: %s", self.name,
                                     value)
            return min_value
        if (max_value is not None) and (value > max_value):
            self.xknx.logger.warning("max value exceeded at %s: %s", self.name,
                                     value)
            return max_value
        return value

    async def set_setpoint_shift(self, offset):
        """Send new temperature offset to KNX bus."""
        validated_offset = self.validate_value(offset, self.setpoint_shift_min,
                                               self.setpoint_shift_max)
        base_temperature = self.base_temperature
        await self._setpoint_shift.set(validated_offset)
        # broadcast new target temperature and set internally
        if self.target_temperature.writable and \
                base_temperature is not None:
            await self.target_temperature.set(base_temperature +
                                              self.setpoint_shift)

    @property
    def target_temperature_max(self):
        """Return the highest possible target temperature."""
        if self.max_temp is not None:
            return self.max_temp
        if self.initialized_for_setpoint_shift_calculations:
            return self.base_temperature + self.setpoint_shift_max
        return None

    @property
    def target_temperature_min(self):
        """Return the lowest possible target temperature."""
        if self.min_temp is not None:
            return self.min_temp
        if self.initialized_for_setpoint_shift_calculations:
            return self.base_temperature + self.setpoint_shift_min
        return None

    async def process_group_write(self, telegram):
        """Process incoming GROUP WRITE telegram."""
        await self.temperature.process(telegram)
        await self.target_temperature.process(telegram)
        await self._setpoint_shift.process(telegram)
        await self.on.process(telegram)
        if self.mode is not None:
            await self.mode.process_group_write(telegram)

    def state_addresses(self):
        """Return group addresses which should be requested to sync state."""
        state_addresses = []
        state_addresses.extend(self.temperature.state_addresses())
        state_addresses.extend(self.target_temperature.state_addresses())
        state_addresses.extend(self._setpoint_shift.state_addresses())
        if self.supports_on_off:
            state_addresses.extend(self.on.state_addresses())
        if self.mode is not None:
            state_addresses.extend(self.mode.state_addresses())
        return state_addresses

    def __str__(self):
        """Return object as readable string."""
        return '<Climate name="{0}" ' \
            'temperature="{1}" ' \
            'target_temperature="{2}" ' \
            'setpoint_shift="{3}" ' \
            'setpoint_shift_step="{4}" ' \
            'setpoint_shift_max="{5}" ' \
            'setpoint_shift_min="{6}" ' \
            'group_address_on_off="{7}" ' \
            '/>' \
            .format(
                self.name,
                self.temperature.group_addr_str(),
                self.target_temperature.group_addr_str(),
                self._setpoint_shift.group_addr_str(),
                self._setpoint_shift.setpoint_shift_step,
                self.setpoint_shift_max,
                self.setpoint_shift_min,
                self.on.group_addr_str())

    def __eq__(self, other):
        """Equal operator."""
        return self.__dict__ == other.__dict__
Пример #17
0
class Cover(Device):
    """Class for managing a cover."""

    DEFAULT_TRAVEL_TIME_DOWN = 22
    DEFAULT_TRAVEL_TIME_UP = 22

    def __init__(
        self,
        xknx: XKNX,
        name: str,
        group_address_long: GroupAddressesType | None = None,
        group_address_short: GroupAddressesType | None = None,
        group_address_stop: GroupAddressesType | None = None,
        group_address_position: GroupAddressesType | None = None,
        group_address_position_state: GroupAddressesType | None = None,
        group_address_angle: GroupAddressesType | None = None,
        group_address_angle_state: GroupAddressesType | None = None,
        group_address_locked_state: GroupAddressesType | None = None,
        travel_time_down: float = DEFAULT_TRAVEL_TIME_DOWN,
        travel_time_up: float = DEFAULT_TRAVEL_TIME_UP,
        invert_position: bool = False,
        invert_angle: bool = False,
        device_updated_cb: DeviceCallbackType | None = None,
        device_class: str | None = None,
    ):
        """Initialize Cover class."""
        super().__init__(xknx, name, device_updated_cb)
        # self.after_update for position changes is called after updating the
        # travelcalculator (in process_group_write and set_*) - angle changes
        # are updated from RemoteValue objects
        self.updown = RemoteValueUpDown(
            xknx,
            group_address_long,
            device_name=self.name,
            after_update_cb=None,
            invert=invert_position,
        )

        self.step = RemoteValueStep(
            xknx,
            group_address_short,
            device_name=self.name,
            after_update_cb=self.after_update,
            invert=invert_position,
        )

        self.stop_ = RemoteValueSwitch(
            xknx,
            group_address=group_address_stop,
            device_name=self.name,
            after_update_cb=None,
        )

        position_range_from = 100 if invert_position else 0
        position_range_to = 0 if invert_position else 100
        self.position_current = RemoteValueScaling(
            xknx,
            group_address_state=group_address_position_state,
            device_name=self.name,
            feature_name="Position",
            after_update_cb=self._current_position_from_rv,
            range_from=position_range_from,
            range_to=position_range_to,
        )
        self.position_target = RemoteValueScaling(
            xknx,
            group_address=group_address_position,
            device_name=self.name,
            feature_name="Target position",
            after_update_cb=self._target_position_from_rv,
            range_from=position_range_from,
            range_to=position_range_to,
        )

        angle_range_from = 100 if invert_angle else 0
        angle_range_to = 0 if invert_angle else 100
        self.angle = RemoteValueScaling(
            xknx,
            group_address_angle,
            group_address_angle_state,
            device_name=self.name,
            feature_name="Tilt angle",
            after_update_cb=self.after_update,
            range_from=angle_range_from,
            range_to=angle_range_to,
        )

        self.locked = RemoteValueSwitch(
            xknx,
            group_address_state=group_address_locked_state,
            device_name=self.name,
            feature_name="Locked",
            after_update_cb=self.after_update,
        )

        self.travel_time_down = travel_time_down
        self.travel_time_up = travel_time_up

        self.travelcalculator = TravelCalculator(travel_time_down,
                                                 travel_time_up)
        self.travel_direction_tilt: TravelStatus | None = None

        self.device_class = device_class

    def _iter_remote_values(self) -> Iterator[RemoteValue[Any, Any]]:
        """Iterate the devices RemoteValue classes."""
        yield self.updown
        yield self.step
        yield self.stop_
        yield self.position_current
        yield self.position_target
        yield self.angle
        yield self.locked

    @property
    def unique_id(self) -> str | None:
        """Return unique id for this device."""
        return f"{self.updown.group_address}"

    def __str__(self) -> str:
        """Return object as readable string."""
        return ('<Cover name="{}" '
                "updown={} "
                "step={} "
                "stop_={} "
                "position_current={} "
                "position_target={} "
                "angle={} "
                "locked={} "
                'travel_time_down="{}" '
                'travel_time_up="{}" />'.format(
                    self.name,
                    self.updown.group_addr_str(),
                    self.step.group_addr_str(),
                    self.stop_.group_addr_str(),
                    self.position_current.group_addr_str(),
                    self.position_target.group_addr_str(),
                    self.angle.group_addr_str(),
                    self.locked.group_addr_str(),
                    self.travel_time_down,
                    self.travel_time_up,
                ))

    async def set_down(self) -> None:
        """Move cover down."""
        await self.updown.down()
        self.travelcalculator.start_travel_down()
        self.travel_direction_tilt = None
        await self.after_update()

    async def set_up(self) -> None:
        """Move cover up."""
        await self.updown.up()
        self.travelcalculator.start_travel_up()
        self.travel_direction_tilt = None
        await self.after_update()

    async def set_short_down(self) -> None:
        """Move cover short down."""
        await self.step.increase()

    async def set_short_up(self) -> None:
        """Move cover short up."""
        await self.step.decrease()

    async def stop(self) -> None:
        """Stop cover."""
        if self.stop_.writable:
            await self.stop_.on()
        elif self.step.writable:
            if (self.travel_direction_tilt == TravelStatus.DIRECTION_UP
                    or self.travelcalculator.travel_direction
                    == TravelStatus.DIRECTION_UP):
                await self.step.decrease()
            elif (self.travel_direction_tilt == TravelStatus.DIRECTION_DOWN
                  or self.travelcalculator.travel_direction
                  == TravelStatus.DIRECTION_DOWN):
                await self.step.increase()
        else:
            logger.warning("Stop not supported for device %s", self.get_name())
            return
        self.travelcalculator.stop()
        self.travel_direction_tilt = None
        await self.after_update()

    async def set_position(self, position: int) -> None:
        """Move cover to a desginated postion."""
        if not self.position_target.writable:
            # No direct positioning group address defined
            # fully open or close is always possible even if current position is not known
            current_position = self.current_position()
            if current_position is None:
                if position == self.travelcalculator.position_open:
                    await self.updown.up()
                elif position == self.travelcalculator.position_closed:
                    await self.updown.down()
                else:
                    logger.warning(
                        "Current position unknown. Initialize cover by moving to end position."
                    )
                    return
            elif position < current_position:
                await self.updown.up()
            elif position > current_position:
                await self.updown.down()
            self.travelcalculator.start_travel(position)
            await self.after_update()
        else:
            await self.position_target.set(position)

    async def _target_position_from_rv(self) -> None:
        """Update the target postion from RemoteValue (Callback)."""
        new_target = self.position_target.value
        if new_target is not None:
            self.travelcalculator.start_travel(new_target)
            await self.after_update()

    async def _current_position_from_rv(self) -> None:
        """Update the current postion from RemoteValue (Callback)."""
        position_before_update = self.travelcalculator.current_position()
        new_position = self.position_current.value
        if new_position is not None:
            if self.is_traveling():
                self.travelcalculator.update_position(new_position)
            else:
                self.travelcalculator.set_position(new_position)
            if position_before_update != self.travelcalculator.current_position(
            ):
                await self.after_update()

    async def set_angle(self, angle: int) -> None:
        """Move cover to designated angle."""
        if not self.supports_angle:
            logger.warning("Angle not supported for device %s",
                           self.get_name())
            return

        current_angle = self.current_angle()
        self.travel_direction_tilt = (
            TravelStatus.DIRECTION_DOWN if current_angle is not None
            and angle >= current_angle else TravelStatus.DIRECTION_UP)

        await self.angle.set(angle)

    async def auto_stop_if_necessary(self) -> None:
        """Do auto stop if necessary."""
        # If device does not support auto_positioning,
        # we have to stop the device when position is reached,
        # unless device was traveling to fully open
        # or fully closed state.
        if (self.supports_stop and not self.position_target.writable
                and self.position_reached() and not self.is_open()
                and not self.is_closed()):
            await self.stop()

    async def sync(self, wait_for_result: bool = False) -> None:
        """Read states of device from KNX bus."""
        await self.position_current.read_state(wait_for_result=wait_for_result)
        await self.angle.read_state(wait_for_result=wait_for_result)

    async def process_group_write(self, telegram: "Telegram") -> None:
        """Process incoming and outgoing GROUP WRITE telegram."""
        # call after_update to account for travelcalculator changes
        if await self.updown.process(telegram):
            if (not self.is_opening()
                    and self.updown.value == RemoteValueUpDown.Direction.UP):
                self.travelcalculator.start_travel_up()
                await self.after_update()
            elif (not self.is_closing()
                  and self.updown.value == RemoteValueUpDown.Direction.DOWN):
                self.travelcalculator.start_travel_down()
                await self.after_update()
        # stop from bus
        if await self.stop_.process(telegram) or await self.step.process(
                telegram):
            if self.is_traveling():
                self.travelcalculator.stop()
                await self.after_update()

        await self.position_current.process(telegram, always_callback=True)
        await self.position_target.process(telegram)
        await self.angle.process(telegram)
        await self.locked.process(telegram)

    def current_position(self) -> int | None:
        """Return current position of cover."""
        return self.travelcalculator.current_position()

    def current_angle(self) -> int | None:
        """Return current tilt angle of cover."""
        return self.angle.value

    def is_locked(self) -> bool | None:
        """Return if the cover is currently locked for manual movement."""
        return self.locked.value

    def is_traveling(self) -> bool:
        """Return if cover is traveling at the moment."""
        return self.travelcalculator.is_traveling()

    def position_reached(self) -> bool:
        """Return if cover has reached its final position."""
        return self.travelcalculator.position_reached()

    def is_open(self) -> bool:
        """Return if cover is open."""
        return self.travelcalculator.is_open()

    def is_closed(self) -> bool:
        """Return if cover is closed."""
        return self.travelcalculator.is_closed()

    def is_opening(self) -> bool:
        """Return if the cover is opening or not."""
        return self.travelcalculator.is_opening()

    def is_closing(self) -> bool:
        """Return if the cover is closing or not."""
        return self.travelcalculator.is_closing()

    @property
    def supports_stop(self) -> bool:
        """Return if cover supports manual stopping."""
        return self.stop_.writable or self.step.writable

    @property
    def supports_locked(self) -> bool:
        """Return if cover supports locking."""
        return self.locked.initialized

    @property
    def supports_position(self) -> bool:
        """Return if cover supports direct positioning."""
        return self.position_target.initialized

    @property
    def supports_angle(self) -> bool:
        """Return if cover supports tilt angle."""
        return self.angle.initialized