From a49a31d0f30312b9ec19c095aaf133acf95218dc Mon Sep 17 00:00:00 2001 From: Daniel Muehlbachler-Pietrzykowski Date: Wed, 15 Nov 2023 17:22:30 +0100 Subject: [PATCH] feat: implement support for weather sensors; related to #72 BREAKING CHANGE: weather sensors are untested due to lack of device --- .../34f0a451a6f9a93edd9319e7ea3b4799.json | 1 + .../359a7719b0d3d1733d4ea7585ac85b43.json | 1 + .../4b3eb8a36c0aee37871a4fb623888e11.json | 1 + .../6518247dceaba6364fa1df5d0ea938df.json | 1 + .../975e7a37850e323f5cb88e2c904ec06e.json | 2 +- .../e04f20f45df6269780bfca68615a220e.json | 2 +- README.md | 8 +- custom_components/hella_onyx/cover.py | 310 +------------ custom_components/hella_onyx/enum/__init__.py | 1 + .../hella_onyx/{ => enum}/moving_state.py | 0 custom_components/hella_onyx/manifest.json | 2 +- custom_components/hella_onyx/sensor.py | 73 +-- .../hella_onyx/sensors/__init__.py | 1 + .../hella_onyx/sensors/device_type.py | 26 ++ .../hella_onyx/{ => sensors}/onyx_entity.py | 6 +- .../hella_onyx/sensors/shutter.py | 317 +++++++++++++ .../hella_onyx/sensors/weather.py | 220 +++++++++ custom_components/hella_onyx/strings.json | 2 +- .../hella_onyx/translations/en.json | 2 +- hacs.json | 2 +- poetry.lock | 381 ++++++++-------- pyproject.toml | 4 +- tests/sensors/__init__.py | 1 + tests/sensors/test_device_type.py | 51 +++ tests/{ => sensors}/test_onyx_entity.py | 4 +- tests/sensors/test_shutter.py | 424 ++++++++++++++++++ tests/sensors/test_weather.py | 379 ++++++++++++++++ tests/test_cover.py | 414 +---------------- tests/test_sensor.py | 72 ++- 29 files changed, 1709 insertions(+), 999 deletions(-) create mode 100644 .flakeheaven_cache/34f0a451a6f9a93edd9319e7ea3b4799.json create mode 100644 .flakeheaven_cache/359a7719b0d3d1733d4ea7585ac85b43.json create mode 100644 .flakeheaven_cache/4b3eb8a36c0aee37871a4fb623888e11.json create mode 100644 .flakeheaven_cache/6518247dceaba6364fa1df5d0ea938df.json create mode 100644 custom_components/hella_onyx/enum/__init__.py rename custom_components/hella_onyx/{ => enum}/moving_state.py (100%) create mode 100644 custom_components/hella_onyx/sensors/__init__.py create mode 100644 custom_components/hella_onyx/sensors/device_type.py rename custom_components/hella_onyx/{ => sensors}/onyx_entity.py (88%) create mode 100644 custom_components/hella_onyx/sensors/shutter.py create mode 100644 custom_components/hella_onyx/sensors/weather.py create mode 100644 tests/sensors/__init__.py create mode 100644 tests/sensors/test_device_type.py rename tests/{ => sensors}/test_onyx_entity.py (91%) create mode 100644 tests/sensors/test_shutter.py create mode 100644 tests/sensors/test_weather.py diff --git a/.flakeheaven_cache/34f0a451a6f9a93edd9319e7ea3b4799.json b/.flakeheaven_cache/34f0a451a6f9a93edd9319e7ea3b4799.json new file mode 100644 index 0000000..e56fbc9 --- /dev/null +++ b/.flakeheaven_cache/34f0a451a6f9a93edd9319e7ea3b4799.json @@ -0,0 +1 @@ +{"results": [["pycodestyle", "W503", 83, 12, "line break before binary operator", " | SUPPORT_CLOSE\n"], ["pycodestyle", "W503", 84, 12, "line break before binary operator", " | SUPPORT_STOP\n"], ["pycodestyle", "W503", 85, 12, "line break before binary operator", " | SUPPORT_SET_POSITION\n"], ["pycodestyle", "W503", 86, 12, "line break before binary operator", " | SUPPORT_SET_TILT_POSITION\n"]], "digest": "c45599c4be9bfa4068ff1f488c7a91fa"} \ No newline at end of file diff --git a/.flakeheaven_cache/359a7719b0d3d1733d4ea7585ac85b43.json b/.flakeheaven_cache/359a7719b0d3d1733d4ea7585ac85b43.json new file mode 100644 index 0000000..3d64cf6 --- /dev/null +++ b/.flakeheaven_cache/359a7719b0d3d1733d4ea7585ac85b43.json @@ -0,0 +1 @@ +{"results": [["pycodestyle", "W503", 48, 12, "line break before binary operator", " or item[1].device_type == DeviceType.WEATHER,\n"]], "digest": "757cdf44b36b18d79ae7a57cc0720e51"} \ No newline at end of file diff --git a/.flakeheaven_cache/4b3eb8a36c0aee37871a4fb623888e11.json b/.flakeheaven_cache/4b3eb8a36c0aee37871a4fb623888e11.json new file mode 100644 index 0000000..feb7370 --- /dev/null +++ b/.flakeheaven_cache/4b3eb8a36c0aee37871a4fb623888e11.json @@ -0,0 +1 @@ +{"results": [["pyflakes", "F401", 11, 0, "'onyx_client.enum.action.Action' imported but unused", "from onyx_client.enum.action import Action\n"]], "digest": "8b3ccf97c1f64be4907c107eca2e6f1c"} \ No newline at end of file diff --git a/.flakeheaven_cache/6518247dceaba6364fa1df5d0ea938df.json b/.flakeheaven_cache/6518247dceaba6364fa1df5d0ea938df.json new file mode 100644 index 0000000..e3f8f34 --- /dev/null +++ b/.flakeheaven_cache/6518247dceaba6364fa1df5d0ea938df.json @@ -0,0 +1 @@ +{"results": [["pycodestyle", "W503", 260, 16, "line break before binary operator", " + position_keyframe.duration\n"], ["pycodestyle", "W503", 261, 16, "line break before binary operator", " + position_keyframe.delay\n"], ["pycodestyle", "W503", 284, 16, "line break before binary operator", " or (position_end_time is None and current_time > angle_end_time)\n"], ["pycodestyle", "W503", 285, 16, "line break before binary operator", " or (current_time > position_end_time and current_time > angle_end_time)\n"]], "digest": "4d64e826b79067d7b67495f87f725b3f"} \ No newline at end of file diff --git a/.flakeheaven_cache/975e7a37850e323f5cb88e2c904ec06e.json b/.flakeheaven_cache/975e7a37850e323f5cb88e2c904ec06e.json index 6829cb8..00e03ec 100644 --- a/.flakeheaven_cache/975e7a37850e323f5cb88e2c904ec06e.json +++ b/.flakeheaven_cache/975e7a37850e323f5cb88e2c904ec06e.json @@ -1 +1 @@ -{"results": [["pycodestyle", "W503", 282, 16, "line break before binary operator", " + position_keyframe.duration\n"], ["pycodestyle", "W503", 283, 16, "line break before binary operator", " + position_keyframe.delay\n"], ["pycodestyle", "W503", 306, 16, "line break before binary operator", " or (position_end_time is None and current_time > angle_end_time)\n"], ["pycodestyle", "W503", 307, 16, "line break before binary operator", " or (current_time > position_end_time and current_time > angle_end_time)\n"]], "digest": "dd31060492c965047fa90dcd5a336b37"} \ No newline at end of file +{"results": [["pyflakes", "F401", 3, 0, "'typing.Any' imported but unused", "from typing import Any, Callable, Optional\n"]], "digest": "3b8c1e5c9fa3df334fee8ed34220d9f8"} \ No newline at end of file diff --git a/.flakeheaven_cache/e04f20f45df6269780bfca68615a220e.json b/.flakeheaven_cache/e04f20f45df6269780bfca68615a220e.json index d8457cc..0994f65 100644 --- a/.flakeheaven_cache/e04f20f45df6269780bfca68615a220e.json +++ b/.flakeheaven_cache/e04f20f45df6269780bfca68615a220e.json @@ -1 +1 @@ -{"results": [["pycodestyle", "W503", 156, 12, "line break before binary operator", " | SUPPORT_CLOSE\n"], ["pycodestyle", "W503", 157, 12, "line break before binary operator", " | SUPPORT_STOP\n"], ["pycodestyle", "W503", 158, 12, "line break before binary operator", " | SUPPORT_SET_POSITION\n"], ["pycodestyle", "W503", 159, 12, "line break before binary operator", " | SUPPORT_SET_TILT_POSITION\n"]], "digest": "c77c271510873caa3a064cd768028129"} \ No newline at end of file +{"results": [["pycodestyle", "W503", 159, 12, "line break before binary operator", " | SUPPORT_CLOSE\n"], ["pycodestyle", "W503", 160, 12, "line break before binary operator", " | SUPPORT_STOP\n"], ["pycodestyle", "W503", 161, 12, "line break before binary operator", " | SUPPORT_SET_POSITION\n"], ["pycodestyle", "W503", 162, 12, "line break before binary operator", " | SUPPORT_SET_TILT_POSITION\n"]], "digest": "76e1ee761d561c52097f5d15562473ec"} \ No newline at end of file diff --git a/README.md b/README.md index e3bc6ed..822d9a6 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,13 @@ [![](https://img.shields.io/github/actions/workflow/status/muhlba91/onyx-homeassistant-integration/release.yml?style=for-the-badge)](https://github.com/muhlba91/onyx-homeassistant-integration/actions/workflows/release.yml) [![](https://img.shields.io/coveralls/github/muhlba91/onyx-homeassistant-integration?style=for-the-badge)](https://github.com/muhlba91/onyx-homeassistant-integration/) [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/custom-components/hacs) -[![Known Vulnerabilities](https://snyk.io/test/github/muhlba91/onyx-homeassistant-integration/badge.svg)](https://snyk.io/test/github/muhlba91/onyx-homeassistant-integration/) +[![Known Vulnerabilities](https://snyk.io/test/github/muhlba91/onyx-homeassistant-integration/badge.svg)](https://snyk.io/test/github/muhlba91/onyx-homeassistant-integration) Buy Me A Coffee -This component creates an integration that provides **raffstore/shutter entities** to control -[Hella's ONYX.CENTER](https://www.hella.info/) via Home Assistant. +This component creates an integration that provides the following entities to control [Hella's ONYX.CENTER](https://www.hella.info/) via Home Assistant: + +- **raffstore/shutter** entities +- **weather station** sensor entities --- diff --git a/custom_components/hella_onyx/cover.py b/custom_components/hella_onyx/cover.py index 860a2b2..dc5d1d6 100644 --- a/custom_components/hella_onyx/cover.py +++ b/custom_components/hella_onyx/cover.py @@ -1,38 +1,14 @@ """The ONYX shutter entity.""" -import asyncio import logging -import time -from datetime import timedelta -from math import ceil -from typing import Any, Callable, Optional +from typing import Callable, Optional -from homeassistant.components.cover import ( - ATTR_POSITION, - ATTR_TILT_POSITION, - CoverDeviceClass, - CoverEntity, - SUPPORT_CLOSE, - SUPPORT_OPEN, - SUPPORT_SET_POSITION, - SUPPORT_SET_TILT_POSITION, - SUPPORT_STOP, -) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.event import ( - track_point_in_utc_time, -) from homeassistant.helpers.typing import DiscoveryInfoType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from homeassistant.util import utcnow -from onyx_client.data.animation_value import AnimationValue -from onyx_client.enum.action import Action -from onyx_client.enum.device_type import DeviceType -from . import APIConnector, DOMAIN, ONYX_TIMEZONE -from .const import INCREASED_INTERVAL_DELTA, ONYX_API, ONYX_COORDINATOR -from .moving_state import MovingState -from .onyx_entity import OnyxEntity +from custom_components.hella_onyx import DOMAIN, ONYX_TIMEZONE +from custom_components.hella_onyx.const import ONYX_API, ONYX_COORDINATOR +from custom_components.hella_onyx.sensors.shutter import OnyxShutter _LOGGER = logging.getLogger(__name__) @@ -59,281 +35,3 @@ async def async_setup_entry( ] _LOGGER.info("adding %s hella_onyx shutter entities", len(shutters)) async_add_entities(shutters, True) - - -class OnyxShutter(OnyxEntity, CoverEntity): - """A shutter entity.""" - - def __init__( - self, - api: APIConnector, - timezone: str, - coordinator: DataUpdateCoordinator, - name: str, - device_type: DeviceType, - uuid: str, - ): - """Initialize a shutter entity.""" - super().__init__(api, timezone, coordinator, name, device_type, uuid) - self._moving_state = MovingState.STILL - - @property - def name(self) -> str: - """Return the display name of the light.""" - return self._name - - @property - def unique_id(self) -> str: - """Return the unique id of the light.""" - return f"{self._uuid}/Shutter" - - @property - def device_class(self) -> Optional[str]: - """Return the class of this device, from component device class.""" - return CoverDeviceClass.SHUTTER - - @property - def supported_features(self): - """Flag supported features.""" - supported_features = ( - SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION - ) - - if ( - self._type - in (DeviceType.RAFFSTORE_90, DeviceType.RAFFSTORE_180) - is not None - ): - supported_features |= SUPPORT_SET_TILT_POSITION - - return supported_features - - @property - def current_cover_position(self) -> int: - """Return current position of cover. - - None is unknown, 0 is closed, 100 is fully open. - """ - position = self._device.actual_position - _LOGGER.debug( - "received position fo device %s: %s (%s/%s)", - self._uuid, - position.value, - position.minimum, - position.maximum, - ) - if position.animation is not None and len(position.animation.keyframes) > 0: - self._start_moving_device(position.animation) - return 100 - int(position.value / position.maximum * 100) - - @property - def current_cover_tilt_position(self) -> int: - """Return current position of cover tilt. - - None is unknown, 0 is closed, 100 is fully open. - """ - position = self._device.actual_angle - _LOGGER.debug( - "received tilt position fo device %s: %s (%s/%s)", - self._uuid, - position.value, - position.minimum, - position.maximum, - ) - if position.animation is not None and len(position.animation.keyframes) > 0: - self._start_moving_device(position.animation) - return int(position.value / self._max_angle * 100) - - @property - def is_opening(self) -> bool: - """Return if the cover is opening or not.""" - return self._moving_state == MovingState.OPENING - - @property - def is_closing(self) -> bool: - """Return if the cover is closing or not.""" - return self._moving_state == MovingState.CLOSING - - @property - def is_closed(self) -> bool: - """Return if the cover is closed or not.""" - position = self._device.actual_position - return position.value == position.maximum - - def open_cover(self, **kwargs: Any) -> None: - """Open the cover.""" - self._set_state(MovingState.OPENING) - asyncio.run_coroutine_threadsafe( - self.api.send_device_command_action(self._uuid, Action.OPEN), self.hass.loop - ) - - def close_cover(self, **kwargs: Any) -> None: - """Close cover.""" - self._set_state(MovingState.CLOSING) - asyncio.run_coroutine_threadsafe( - self.api.send_device_command_action(self._uuid, Action.CLOSE), - self.hass.loop, - ) - - def set_cover_position(self, **kwargs): - """Move the cover to a specific position.""" - if ATTR_POSITION in kwargs: - position = 100 - int(kwargs.get(ATTR_POSITION)) - hella_position = ceil( - position * (self._device.target_position.maximum / 100) - ) - self._calculate_and_set_state(self._device.actual_position.value, position) - asyncio.run_coroutine_threadsafe( - self.api.send_device_command_properties( - self._uuid, - { - "target_position": hella_position, - "target_angle": self._device.target_angle.value, - }, - ), - self.hass.loop, - ) - - def stop_cover(self, **kwargs): - """Stop the cover.""" - self._set_state(MovingState.STILL) - asyncio.run_coroutine_threadsafe( - self.api.send_device_command_action(self._uuid, Action.STOP), self.hass.loop - ) - - def open_cover_tilt(self, **kwargs: Any) -> None: - """Open the cover tilt.""" - raise NotImplementedError() - - def close_cover_tilt(self, **kwargs: Any) -> None: - """Close the cover tilt.""" - raise NotImplementedError() - - def set_cover_tilt_position(self, **kwargs): - """Move the cover tilt to a specific position.""" - if ATTR_TILT_POSITION in kwargs: - angle = int(kwargs.get(ATTR_TILT_POSITION)) - hella_angle = ceil(angle * (self._max_angle / 100)) - self._calculate_and_set_state( - self._device.actual_angle.value, - hella_angle, - ) - asyncio.run_coroutine_threadsafe( - self.api.send_device_command_properties( - self._uuid, {"target_angle": hella_angle} - ), - self.hass.loop, - ) - - def stop_cover_tilt(self, **kwargs): - """Stop the cover.""" - self._set_state(MovingState.STILL) - asyncio.run_coroutine_threadsafe( - self.api.send_device_command_action(self._uuid, Action.STOP), self.hass.loop - ) - - def _set_state(self, state: MovingState): - """Set the new moving state.""" - self._moving_state = state - - def _start_moving_device(self, animation: AnimationValue): - """Start the update loop.""" - if self._moving_state == MovingState.STILL: - _LOGGER.debug("not moving still device %s", self._uuid) - return - - keyframes = len(animation.keyframes) - keyframe = animation.keyframes[keyframes - 1] - - current_time = time.time() - end_time = animation.start + keyframe.duration + keyframe.delay - delta = end_time - current_time - moving = current_time < end_time - - _LOGGER.debug( - "moving device %s with current_time %s and end_time %s: %s", - self._uuid, - current_time, - end_time, - moving, - ) - - if moving: - track_point_in_utc_time( - self.hass, - self._end_moving_device, - utcnow() + timedelta(seconds=delta + INCREASED_INTERVAL_DELTA), - ) - else: - _LOGGER.debug("end moving device %s due to too old data", self._uuid) - self._end_moving_device() - - def _end_moving_device(self, *args: Any): - """Call STOP to update the device values on ONYX.""" - position_animation = self._device.actual_position.animation - position_keyframe = ( - position_animation.keyframes[len(position_animation.keyframes) - 1] - if position_animation is not None and len(position_animation.keyframes) > 0 - else None - ) - position_end_time = ( - ( - position_animation.start - + position_keyframe.duration - + position_keyframe.delay - ) - if position_keyframe is not None - else None - ) - - angle_animation = self._device.actual_angle.animation - angle_keyframe = ( - angle_animation.keyframes[len(angle_animation.keyframes) - 1] - if angle_animation is not None and len(angle_animation.keyframes) > 0 - else None - ) - angle_end_time = ( - (angle_animation.start + angle_keyframe.duration + angle_keyframe.delay) - if angle_keyframe is not None - else None - ) - - current_time = time.time() - - if self._moving_state != MovingState.STILL: - if ( - (angle_end_time is None and current_time > position_end_time) - or (position_end_time is None and current_time > angle_end_time) - or (current_time > position_end_time and current_time > angle_end_time) - ): - self.stop_cover() - else: - _LOGGER.debug( - "not ending moving device %s. overlapping angle and positioning", - self._uuid, - ) - - def _calculate_and_set_state(self, actual: int, new_value: int): - """Calculate and set the new moving state.""" - new_state = self._calculate_state(actual, new_value) - self._set_state(new_state) - - @property - def _max_angle(self) -> int: - """Maximum angle depending on raffstore type.""" - if self._type == DeviceType.RAFFSTORE_90: - return 90 - elif self._type == DeviceType.RAFFSTORE_180: - return 180 - else: - return 100 - - @staticmethod - def _calculate_state(actual: int, new_value: int) -> MovingState: - """Calculate the new moving state.""" - if new_value < actual: - return MovingState.OPENING - elif new_value > actual: - return MovingState.CLOSING - else: - return MovingState.STILL diff --git a/custom_components/hella_onyx/enum/__init__.py b/custom_components/hella_onyx/enum/__init__.py new file mode 100644 index 0000000..213e670 --- /dev/null +++ b/custom_components/hella_onyx/enum/__init__.py @@ -0,0 +1 @@ +"""The ONYX.CENTER Home Assistant enums.""" diff --git a/custom_components/hella_onyx/moving_state.py b/custom_components/hella_onyx/enum/moving_state.py similarity index 100% rename from custom_components/hella_onyx/moving_state.py rename to custom_components/hella_onyx/enum/moving_state.py diff --git a/custom_components/hella_onyx/manifest.json b/custom_components/hella_onyx/manifest.json index 70ebc66..d4efb1e 100644 --- a/custom_components/hella_onyx/manifest.json +++ b/custom_components/hella_onyx/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://github.com/muhlba91/onyx-homeassistant-integration", "issue_tracker": "https://github.com/muhlba91/onyx-homeassistant-integration/issues", "requirements": [ - "onyx-client==7.1.1" + "onyx-client==7.2.1" ], "ssdp": [], "zeroconf": [], diff --git a/custom_components/hella_onyx/sensor.py b/custom_components/hella_onyx/sensor.py index 41358e6..f654bc1 100644 --- a/custom_components/hella_onyx/sensor.py +++ b/custom_components/hella_onyx/sensor.py @@ -6,9 +6,19 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import DiscoveryInfoType +from onyx_client.enum.device_type import DeviceType + from . import ONYX_API, ONYX_COORDINATOR, ONYX_TIMEZONE from .const import DOMAIN -from .onyx_entity import OnyxEntity +from custom_components.hella_onyx.sensors.device_type import OnyxSensorDeviceType +from custom_components.hella_onyx.sensors.weather import ( + OnyxSensorWeatherHumidity, + OnyxSensorWeatherTemperature, + OnyxSensorWeatherAirPressure, + OnyxSensorWeatherWindPeak, + OnyxSensorWeatherSunBrightnessPeak, + OnyxSensorWeatherSunBrightnessSink, +) _LOGGER = logging.getLogger(__name__) @@ -19,46 +29,53 @@ async def async_setup_entry( async_add_entities: Callable, discovery_info: Optional[DiscoveryInfoType] = None, ): - """Set up the ONYX shutter platform.""" + """Set up the ONYX platform.""" data = hass.data[DOMAIN][entry.entry_id] api = data[ONYX_API] timezone = data[ONYX_TIMEZONE] coordinator = data[ONYX_COORDINATOR] + # all device type sensors sensors = [ [ OnyxSensorDeviceType( api, timezone, coordinator, device.name, device.device_type, device_id ), ] + # we only support shutters or weather stations for device_id, device in filter( - lambda item: item[1].device_type.is_shutter(), api.devices.items() + lambda item: item[1].device_type.is_shutter() + or item[1].device_type == DeviceType.WEATHER, + api.devices.items(), + ) + ] + # all weather stations + sensors = sensors + [ + [ + OnyxSensorWeatherHumidity( + api, timezone, coordinator, device.name, device.device_type, device_id + ), + OnyxSensorWeatherTemperature( + api, timezone, coordinator, device.name, device.device_type, device_id + ), + OnyxSensorWeatherAirPressure( + api, timezone, coordinator, device.name, device.device_type, device_id + ), + OnyxSensorWeatherWindPeak( + api, timezone, coordinator, device.name, device.device_type, device_id + ), + OnyxSensorWeatherSunBrightnessPeak( + api, timezone, coordinator, device.name, device.device_type, device_id + ), + OnyxSensorWeatherSunBrightnessSink( + api, timezone, coordinator, device.name, device.device_type, device_id + ), + ] + for device_id, device in filter( + lambda item: item[1].device_type == DeviceType.WEATHER, api.devices.items() ) ] sensors = [item for sublist in sensors for item in sublist] - _LOGGER.info("adding %s hella_onyx shutter sensor entities", len(sensors)) - async_add_entities(sensors, True) - -class OnyxSensorDeviceType(OnyxEntity): - """ONYX Device Type Sensor.""" - - @property - def name(self) -> str: - """Return the display name of the sensor.""" - return f"{self._name} Device Type" - - @property - def unique_id(self) -> str: - """Return the unique id of the sensor.""" - return f"{self._uuid}/DeviceType" - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return "mdi:cellphone-link" - - @property - def state(self): - """Return the current value.""" - return self._device.device_type.string() + _LOGGER.info("adding %s hella_onyx sensor entities", len(sensors)) + async_add_entities(sensors, True) diff --git a/custom_components/hella_onyx/sensors/__init__.py b/custom_components/hella_onyx/sensors/__init__.py new file mode 100644 index 0000000..56c71ff --- /dev/null +++ b/custom_components/hella_onyx/sensors/__init__.py @@ -0,0 +1 @@ +"""The ONYX.CENTER Home Assistant sensors.""" diff --git a/custom_components/hella_onyx/sensors/device_type.py b/custom_components/hella_onyx/sensors/device_type.py new file mode 100644 index 0000000..bf4cfa5 --- /dev/null +++ b/custom_components/hella_onyx/sensors/device_type.py @@ -0,0 +1,26 @@ +"""The ONYX device type sensor.""" +from custom_components.hella_onyx.sensors.onyx_entity import OnyxEntity + + +class OnyxSensorDeviceType(OnyxEntity): + """ONYX Device Type Sensor.""" + + @property + def name(self) -> str: + """Return the display name of the sensor.""" + return f"{self._name} Device Type" + + @property + def unique_id(self) -> str: + """Return the unique id of the sensor.""" + return f"{self._uuid}/DeviceType" + + @property + def icon(self) -> str: + """Icon to use in the frontend, if any.""" + return "mdi:cellphone-link" + + @property + def state(self) -> str: + """Return the current value.""" + return self._device.device_type.string() diff --git a/custom_components/hella_onyx/onyx_entity.py b/custom_components/hella_onyx/sensors/onyx_entity.py similarity index 88% rename from custom_components/hella_onyx/onyx_entity.py rename to custom_components/hella_onyx/sensors/onyx_entity.py index 24899fb..2d26be7 100644 --- a/custom_components/hella_onyx/onyx_entity.py +++ b/custom_components/hella_onyx/sensors/onyx_entity.py @@ -6,8 +6,8 @@ ) from onyx_client.enum.device_type import DeviceType -from .api_connector import APIConnector -from .const import DOMAIN +from custom_components.hella_onyx.api_connector import APIConnector +from custom_components.hella_onyx.const import DOMAIN class OnyxEntity(CoordinatorEntity): @@ -33,7 +33,7 @@ def __init__( @property def icon(self): """Icon to use in the frontend, if any.""" - return "mdi:window-shutter" + return "mdi:help" @property def device_info(self): diff --git a/custom_components/hella_onyx/sensors/shutter.py b/custom_components/hella_onyx/sensors/shutter.py new file mode 100644 index 0000000..e88ee68 --- /dev/null +++ b/custom_components/hella_onyx/sensors/shutter.py @@ -0,0 +1,317 @@ +"""The ONYX shutter entity.""" +import asyncio +import logging +import time +from datetime import timedelta +from math import ceil +from typing import Any, Optional + +from homeassistant.components.cover import ( + ATTR_POSITION, + ATTR_TILT_POSITION, + CoverDeviceClass, + CoverEntity, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, + SUPPORT_STOP, +) +from homeassistant.helpers.event import ( + track_point_in_utc_time, +) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util import utcnow +from onyx_client.data.animation_value import AnimationValue +from onyx_client.enum.action import Action +from onyx_client.enum.device_type import DeviceType + +from custom_components.hella_onyx.api_connector import APIConnector +from custom_components.hella_onyx.const import INCREASED_INTERVAL_DELTA +from custom_components.hella_onyx.enum.moving_state import MovingState +from custom_components.hella_onyx.sensors.onyx_entity import OnyxEntity + +_LOGGER = logging.getLogger(__name__) + + +class OnyxShutter(OnyxEntity, CoverEntity): + """A shutter entity.""" + + def __init__( + self, + api: APIConnector, + timezone: str, + coordinator: DataUpdateCoordinator, + name: str, + device_type: DeviceType, + uuid: str, + ): + """Initialize a shutter entity.""" + super().__init__(api, timezone, coordinator, name, device_type, uuid) + self._moving_state = MovingState.STILL + + @property + def icon(self) -> str: + """Icon to use in the frontend, if any.""" + return "mdi:window-shutter" + + @property + def name(self) -> str: + """Return the display name of the sensor.""" + return self._name + + @property + def unique_id(self) -> str: + """Return the unique id of the sensor.""" + return f"{self._uuid}/Shutter" + + @property + def device_class(self) -> Optional[str]: + """Return the class of this device, from component device class.""" + return CoverDeviceClass.SHUTTER + + @property + def supported_features(self): + """Flag supported features.""" + supported_features = ( + SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION + ) + + if ( + self._type + in (DeviceType.RAFFSTORE_90, DeviceType.RAFFSTORE_180) + is not None + ): + supported_features |= SUPPORT_SET_TILT_POSITION + + return supported_features + + @property + def current_cover_position(self) -> int: + """Return current position of cover. + + None is unknown, 0 is closed, 100 is fully open. + """ + position = self._device.actual_position + _LOGGER.debug( + "received position fo device %s: %s (%s/%s)", + self._uuid, + position.value, + position.minimum, + position.maximum, + ) + if position.animation is not None and len(position.animation.keyframes) > 0: + self._start_moving_device(position.animation) + return 100 - int(position.value / position.maximum * 100) + + @property + def current_cover_tilt_position(self) -> int: + """Return current position of cover tilt. + + None is unknown, 0 is closed, 100 is fully open. + """ + position = self._device.actual_angle + _LOGGER.debug( + "received tilt position fo device %s: %s (%s/%s)", + self._uuid, + position.value, + position.minimum, + position.maximum, + ) + if position.animation is not None and len(position.animation.keyframes) > 0: + self._start_moving_device(position.animation) + return int(position.value / self._max_angle * 100) + + @property + def is_opening(self) -> bool: + """Return if the cover is opening or not.""" + return self._moving_state == MovingState.OPENING + + @property + def is_closing(self) -> bool: + """Return if the cover is closing or not.""" + return self._moving_state == MovingState.CLOSING + + @property + def is_closed(self) -> bool: + """Return if the cover is closed or not.""" + position = self._device.actual_position + return position.value == position.maximum + + def open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + self._set_state(MovingState.OPENING) + asyncio.run_coroutine_threadsafe( + self.api.send_device_command_action(self._uuid, Action.OPEN), self.hass.loop + ) + + def close_cover(self, **kwargs: Any) -> None: + """Close cover.""" + self._set_state(MovingState.CLOSING) + asyncio.run_coroutine_threadsafe( + self.api.send_device_command_action(self._uuid, Action.CLOSE), + self.hass.loop, + ) + + def set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + if ATTR_POSITION in kwargs: + position = 100 - int(kwargs.get(ATTR_POSITION)) + hella_position = ceil( + position * (self._device.target_position.maximum / 100) + ) + self._calculate_and_set_state(self._device.actual_position.value, position) + asyncio.run_coroutine_threadsafe( + self.api.send_device_command_properties( + self._uuid, + { + "target_position": hella_position, + "target_angle": self._device.target_angle.value, + }, + ), + self.hass.loop, + ) + + def stop_cover(self, **kwargs): + """Stop the cover.""" + self._set_state(MovingState.STILL) + asyncio.run_coroutine_threadsafe( + self.api.send_device_command_action(self._uuid, Action.STOP), self.hass.loop + ) + + def open_cover_tilt(self, **kwargs: Any) -> None: + """Open the cover tilt.""" + raise NotImplementedError() + + def close_cover_tilt(self, **kwargs: Any) -> None: + """Close the cover tilt.""" + raise NotImplementedError() + + def set_cover_tilt_position(self, **kwargs): + """Move the cover tilt to a specific position.""" + if ATTR_TILT_POSITION in kwargs: + angle = int(kwargs.get(ATTR_TILT_POSITION)) + hella_angle = ceil(angle * (self._max_angle / 100)) + self._calculate_and_set_state( + self._device.actual_angle.value, + hella_angle, + ) + asyncio.run_coroutine_threadsafe( + self.api.send_device_command_properties( + self._uuid, {"target_angle": hella_angle} + ), + self.hass.loop, + ) + + def stop_cover_tilt(self, **kwargs): + """Stop the cover.""" + self._set_state(MovingState.STILL) + asyncio.run_coroutine_threadsafe( + self.api.send_device_command_action(self._uuid, Action.STOP), self.hass.loop + ) + + def _set_state(self, state: MovingState): + """Set the new moving state.""" + self._moving_state = state + + def _start_moving_device(self, animation: AnimationValue): + """Start the update loop.""" + if self._moving_state == MovingState.STILL: + _LOGGER.debug("not moving still device %s", self._uuid) + return + + keyframes = len(animation.keyframes) + keyframe = animation.keyframes[keyframes - 1] + + current_time = time.time() + end_time = animation.start + keyframe.duration + keyframe.delay + delta = end_time - current_time + moving = current_time < end_time + + _LOGGER.debug( + "moving device %s with current_time %s and end_time %s: %s", + self._uuid, + current_time, + end_time, + moving, + ) + + if moving: + track_point_in_utc_time( + self.hass, + self._end_moving_device, + utcnow() + timedelta(seconds=delta + INCREASED_INTERVAL_DELTA), + ) + else: + _LOGGER.debug("end moving device %s due to too old data", self._uuid) + self._end_moving_device() + + def _end_moving_device(self, *args: Any): + """Call STOP to update the device values on ONYX.""" + position_animation = self._device.actual_position.animation + position_keyframe = ( + position_animation.keyframes[len(position_animation.keyframes) - 1] + if position_animation is not None and len(position_animation.keyframes) > 0 + else None + ) + position_end_time = ( + ( + position_animation.start + + position_keyframe.duration + + position_keyframe.delay + ) + if position_keyframe is not None + else None + ) + + angle_animation = self._device.actual_angle.animation + angle_keyframe = ( + angle_animation.keyframes[len(angle_animation.keyframes) - 1] + if angle_animation is not None and len(angle_animation.keyframes) > 0 + else None + ) + angle_end_time = ( + (angle_animation.start + angle_keyframe.duration + angle_keyframe.delay) + if angle_keyframe is not None + else None + ) + + current_time = time.time() + + if self._moving_state != MovingState.STILL: + if ( + (angle_end_time is None and current_time > position_end_time) + or (position_end_time is None and current_time > angle_end_time) + or (current_time > position_end_time and current_time > angle_end_time) + ): + self.stop_cover() + else: + _LOGGER.debug( + "not ending moving device %s. overlapping angle and positioning", + self._uuid, + ) + + def _calculate_and_set_state(self, actual: int, new_value: int): + """Calculate and set the new moving state.""" + new_state = self._calculate_state(actual, new_value) + self._set_state(new_state) + + @property + def _max_angle(self) -> int: + """Maximum angle depending on raffstore type.""" + if self._type == DeviceType.RAFFSTORE_90: + return 90 + elif self._type == DeviceType.RAFFSTORE_180: + return 180 + else: + return 100 + + @staticmethod + def _calculate_state(actual: int, new_value: int) -> MovingState: + """Calculate the new moving state.""" + if new_value < actual: + return MovingState.OPENING + elif new_value > actual: + return MovingState.CLOSING + else: + return MovingState.STILL diff --git a/custom_components/hella_onyx/sensors/weather.py b/custom_components/hella_onyx/sensors/weather.py new file mode 100644 index 0000000..0a75bf2 --- /dev/null +++ b/custom_components/hella_onyx/sensors/weather.py @@ -0,0 +1,220 @@ +"""The ONYX weather sensors.""" +from typing import Optional + +from custom_components.hella_onyx.sensors.onyx_entity import OnyxEntity + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, +) +from homeassistant.const import ( + UnitOfTemperature, + UnitOfPressure, + UnitOfSpeed, + LIGHT_LUX, + PERCENTAGE, +) + + +class OnyxSensorWeatherHumidity(OnyxEntity, SensorEntity): + """ONYX Weather Humidity Sensor.""" + + @property + def name(self) -> str: + """Return the display name of the sensor.""" + return f"{self._name} Humidity" + + @property + def unique_id(self) -> str: + """Return the unique id of the sensor.""" + return f"{self._uuid}/Humidity" + + @property + def icon(self) -> str: + """Icon to use in the frontend, if any.""" + return "mdi:water-percent" + + @property + def device_class(self) -> Optional[str]: + """Return the class of this device, from component device class.""" + return SensorDeviceClass.HUMIDITY + + @property + def native_unit_of_measurement(self) -> Optional[str]: + """Return the native unit of this measurement.""" + return PERCENTAGE + + @property + def native_value(self) -> int: + """Return the current value.""" + return self._device.humidity.value + + +class OnyxSensorWeatherTemperature(OnyxEntity, SensorEntity): + """ONYX Weather Temperature Sensor.""" + + @property + def name(self) -> str: + """Return the display name of the sensor.""" + return f"{self._name} Temperature" + + @property + def unique_id(self) -> str: + """Return the unique id of the sensor.""" + return f"{self._uuid}/Temperature" + + @property + def icon(self) -> str: + """Icon to use in the frontend, if any.""" + return "mdi:thermometer" + + @property + def device_class(self) -> Optional[str]: + """Return the class of this device, from component device class.""" + return SensorDeviceClass.TEMPERATURE + + @property + def native_unit_of_measurement(self) -> Optional[str]: + """Return the native unit of this measurement.""" + return UnitOfTemperature.CELSIUS + + @property + def native_value(self) -> float: + """Return the current value.""" + return self._device.temperature.value / 10 + + +class OnyxSensorWeatherAirPressure(OnyxEntity, SensorEntity): + """ONYX Weather Air Pressure Sensor.""" + + @property + def name(self) -> str: + """Return the display name of the sensor.""" + return f"{self._name} Air Pressure" + + @property + def unique_id(self) -> str: + """Return the unique id of the sensor.""" + return f"{self._uuid}/AirPressure" + + @property + def icon(self) -> str: + """Icon to use in the frontend, if any.""" + return "mdi:gauge" + + @property + def device_class(self) -> Optional[str]: + """Return the class of this device, from component device class.""" + return SensorDeviceClass.ATMOSPHERIC_PRESSURE + + @property + def native_unit_of_measurement(self) -> Optional[str]: + """Return the native unit of this measurement.""" + return UnitOfPressure.HPA + + @property + def native_value(self) -> float: + """Return the current value.""" + return self._device.air_pressure.value / 100 + + +class OnyxSensorWeatherWindPeak(OnyxEntity, SensorEntity): + """ONYX Weather Wind Peak Sensor.""" + + @property + def name(self) -> str: + """Return the display name of the sensor.""" + return f"{self._name} Wind Peak" + + @property + def unique_id(self) -> str: + """Return the unique id of the sensor.""" + return f"{self._uuid}/WindPeak" + + @property + def icon(self) -> str: + """Icon to use in the frontend, if any.""" + return "mdi:weather-windy" + + @property + def device_class(self) -> Optional[str]: + """Return the class of this device, from component device class.""" + return SensorDeviceClass.WIND_SPEED + + @property + def native_unit_of_measurement(self) -> Optional[str]: + """Return the native unit of this measurement.""" + return UnitOfSpeed.METERS_PER_SECOND + + @property + def native_value(self) -> float: + """Return the current value.""" + return self._device.wind_peak.value / 1000 + + +class OnyxSensorWeatherSunBrightnessPeak(OnyxEntity, SensorEntity): + """ONYX Weather Sun Brightness Peak Sensor.""" + + @property + def name(self) -> str: + """Return the display name of the sensor.""" + return f"{self._name} Sun Brightness Peak" + + @property + def unique_id(self) -> str: + """Return the unique id of the sensor.""" + return f"{self._uuid}/SunBrightnessPeak" + + @property + def icon(self) -> str: + """Icon to use in the frontend, if any.""" + return "mdi:weather-sunset-up" + + @property + def device_class(self) -> Optional[str]: + """Return the class of this device, from component device class.""" + return SensorDeviceClass.ILLUMINANCE + + @property + def native_unit_of_measurement(self) -> Optional[str]: + """Return the native unit of this measurement.""" + return LIGHT_LUX + + @property + def native_value(self) -> float: + """Return the current value.""" + return self._device.sun_brightness_peak.value + + +class OnyxSensorWeatherSunBrightnessSink(OnyxEntity, SensorEntity): + """ONYX Weather Sun Brightness Sink Sensor.""" + + @property + def name(self) -> str: + """Return the display name of the sensor.""" + return f"{self._name} Sun Brightness Sink" + + @property + def unique_id(self) -> str: + """Return the unique id of the sensor.""" + return f"{self._uuid}/SunBrightnessSink" + + @property + def icon(self) -> str: + """Icon to use in the frontend, if any.""" + return "mdi:weather-sunset-down" + + @property + def device_class(self) -> Optional[str]: + """Return the class of this device, from component device class.""" + return SensorDeviceClass.ILLUMINANCE + + @property + def native_unit_of_measurement(self) -> Optional[str]: + """Return the native unit of this measurement.""" + return LIGHT_LUX + + @property + def native_value(self) -> float: + """Return the current value.""" + return self._device.sun_brightness_sink.value diff --git a/custom_components/hella_onyx/strings.json b/custom_components/hella_onyx/strings.json index b973dea..f4af239 100644 --- a/custom_components/hella_onyx/strings.json +++ b/custom_components/hella_onyx/strings.json @@ -19,4 +19,4 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } -} +} \ No newline at end of file diff --git a/custom_components/hella_onyx/translations/en.json b/custom_components/hella_onyx/translations/en.json index 75b5c15..6144187 100644 --- a/custom_components/hella_onyx/translations/en.json +++ b/custom_components/hella_onyx/translations/en.json @@ -19,4 +19,4 @@ } }, "title": "Hella ONYX.CENTER" -} +} \ No newline at end of file diff --git a/hacs.json b/hacs.json index a80fdf5..c2e276c 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ { "name": "Hella ONYX.CENTER", - "homeassistant": "2023.8.0", + "homeassistant": "2023.11.0", "render_readme": true } \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index b3f03f7..7872b4c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand. [[package]] name = "aiohttp" @@ -233,13 +233,13 @@ frozenlist = ">=1.1.0" [[package]] name = "anyio" -version = "3.7.1" +version = "4.0.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, - {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, + {file = "anyio-4.0.0-py3-none-any.whl", hash = "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f"}, + {file = "anyio-4.0.0.tar.gz", hash = "sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"}, ] [package.dependencies] @@ -247,9 +247,9 @@ idna = ">=2.8" sniffio = ">=1.1" [package.extras] -doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] -test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (<0.22)"] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.22)"] [[package]] name = "astral" @@ -403,75 +403,63 @@ files = [ [[package]] name = "cffi" -version = "1.15.1" +version = "1.16.0" description = "Foreign Function Interface for Python calling C code." optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, - {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, - {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, - {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, - {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, - {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, - {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, - {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, - {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, - {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, - {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, ] [package.dependencies] @@ -490,86 +478,101 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.2.0" +version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, - {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] [[package]] @@ -784,18 +787,19 @@ files = [ [[package]] name = "filelock" -version = "3.12.3" +version = "3.13.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.12.3-py3-none-any.whl", hash = "sha256:f067e40ccc40f2b48395a80fcbd4728262fab54e232e090a4063ab804179efeb"}, - {file = "filelock-3.12.3.tar.gz", hash = "sha256:0ecc1dd2ec4672a10c8550a8182f1bd0c0a5088470ecd5a125e45f49472fac3d"}, + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, ] [package.extras] -docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] [[package]] name = "flake8" @@ -1016,13 +1020,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "identify" -version = "2.5.27" +version = "2.5.31" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.27-py2.py3-none-any.whl", hash = "sha256:fdb527b2dfe24602809b2201e033c2a113d7bdf716db3ca8e3243f735dcecaba"}, - {file = "identify-2.5.27.tar.gz", hash = "sha256:287b75b04a0e22d727bc9a41f0d4f3c1bcada97490fa6eabb5b28f0e9097e733"}, + {file = "identify-2.5.31-py2.py3-none-any.whl", hash = "sha256:90199cb9e7bd3c5407a9b7e81b4abec4bb9d249991c79439ec8af740afc6293d"}, + {file = "identify-2.5.31.tar.gz", hash = "sha256:7736b3c7a28233637e3c36550646fc6389bedd74ae84cb788200cc8e2dd60b75"}, ] [package.extras] @@ -1362,17 +1366,17 @@ setuptools = "*" [[package]] name = "onyx-client" -version = "7.1.1" +version = "7.2.1" description = "HTTP Client for Hella's ONYX.CENTER API." optional = false python-versions = ">=3.11,<4.0" files = [ - {file = "onyx_client-7.1.1-py3-none-any.whl", hash = "sha256:592dd21cc29878838bbe9e8952bc32875a3ff84557c8ad56e9c861b6d153ae52"}, - {file = "onyx_client-7.1.1.tar.gz", hash = "sha256:587092c6feb1665cc74f8b5e74068baa8a5f252919064c3a331d79ca5409f89a"}, + {file = "onyx_client-7.2.1-py3-none-any.whl", hash = "sha256:7a3b0810508b8a73a1c8e36392ff41479ab830afb8925acf85145a338cfd5f1f"}, + {file = "onyx_client-7.2.1.tar.gz", hash = "sha256:6070f54cf91c0f24909b469867bf0d6d72200c95429613aec8fd7c1aaf3d9c63"}, ] [package.dependencies] -aiohttp = ">=3.8.4,<4.0.0" +aiohttp = ">=3.8.5,<4.0.0" [[package]] name = "orjson" @@ -1435,13 +1439,13 @@ files = [ [[package]] name = "packaging" -version = "23.1" +version = "23.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] [[package]] @@ -1457,24 +1461,24 @@ files = [ [[package]] name = "pip" -version = "23.2.1" +version = "23.3.1" description = "The PyPA recommended tool for installing Python packages." optional = false python-versions = ">=3.7" files = [ - {file = "pip-23.2.1-py3-none-any.whl", hash = "sha256:7ccf472345f20d35bdc9d1841ff5f313260c2c33fe417f48c30ac46cccabf5be"}, - {file = "pip-23.2.1.tar.gz", hash = "sha256:fb0bd5435b3200c602b5bf61d2d43c2f13c02e29c1707567ae7fbc514eb9faf2"}, + {file = "pip-23.3.1-py3-none-any.whl", hash = "sha256:55eb67bb6171d37447e82213be585b75fe2b12b359e993773aca4de9247a052b"}, + {file = "pip-23.3.1.tar.gz", hash = "sha256:1fcaa041308d01f14575f6d0d2ea4b75a3e2871fe4f9c694976f908768e14174"}, ] [[package]] name = "platformdirs" -version = "3.10.0" +version = "3.11.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, - {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, + {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, + {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, ] [package.extras] @@ -1670,13 +1674,13 @@ unidecode = ["Unidecode (>=1.1.1)"] [[package]] name = "pytz" -version = "2023.3" +version = "2023.3.post1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, - {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, + {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, + {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, ] [[package]] @@ -1761,19 +1765,19 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "setuptools" -version = "68.1.2" +version = "68.2.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-68.1.2-py3-none-any.whl", hash = "sha256:3d8083eed2d13afc9426f227b24fd1659489ec107c0e86cec2ffdde5c92e790b"}, - {file = "setuptools-68.1.2.tar.gz", hash = "sha256:3d4dfa6d95f1b101d695a6160a7626e15583af71a5f52176efa5d39a054d475d"}, + {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, + {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5,<=7.1.2)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "sniffio" @@ -1857,30 +1861,29 @@ files = [ [[package]] name = "urllib3" -version = "2.0.4" +version = "2.1.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"}, - {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"}, + {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, + {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.24.3" +version = "20.24.6" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.24.3-py3-none-any.whl", hash = "sha256:95a6e9398b4967fbcb5fef2acec5efaf9aa4972049d9ae41f95e0972a683fd02"}, - {file = "virtualenv-20.24.3.tar.gz", hash = "sha256:e5c3b4ce817b0b328af041506a2a299418c98747c4b1e68cb7527e74ced23efc"}, + {file = "virtualenv-20.24.6-py3-none-any.whl", hash = "sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381"}, + {file = "virtualenv-20.24.6.tar.gz", hash = "sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af"}, ] [package.dependencies] @@ -1889,7 +1892,7 @@ filelock = ">=3.12.2,<4" platformdirs = ">=3.9.1,<4" [package.extras] -docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [[package]] @@ -2007,4 +2010,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "36a8917c9ef4777c878fec95d5bc6de1e2ad4de46efe85543c4eb0e2f0541606" +content-hash = "c028f9cbcd79f66482514aeb905b06b14ea3da67fa915675f78e5825148aa81f" diff --git a/pyproject.toml b/pyproject.toml index e5aa2a7..e62225f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.11" -onyx-client = "^7.1.1" +onyx-client = "^7.2.1" [tool.poetry.dev-dependencies] pytest = "^7.3.1" @@ -39,7 +39,7 @@ black = "^23.3.0" pre-commit = "^3.3.2" pytest-asyncio = "^0.21.0" aioresponses = "^0.7.4" -homeassistant = "^2023.8.0" +homeassistant = "^2023.11.0" [tool.pytest.ini_options] minversion = "6.0" diff --git a/tests/sensors/__init__.py b/tests/sensors/__init__.py new file mode 100644 index 0000000..f5b1af4 --- /dev/null +++ b/tests/sensors/__init__.py @@ -0,0 +1 @@ +"""Tests for the ONYX.CENTER sensors.""" diff --git a/tests/sensors/test_device_type.py b/tests/sensors/test_device_type.py new file mode 100644 index 0000000..7a5478b --- /dev/null +++ b/tests/sensors/test_device_type.py @@ -0,0 +1,51 @@ +"""Test for the ONYX Device Type Sensor.""" + +from unittest.mock import MagicMock + +import pytest +from onyx_client.data.device_mode import DeviceMode +from onyx_client.device.shutter import Shutter +from onyx_client.enum.action import Action +from onyx_client.enum.device_type import DeviceType + +from custom_components.hella_onyx.sensors.device_type import OnyxSensorDeviceType + + +class TestOnyxSensorDeviceType: + @pytest.fixture + def api(self): + yield MagicMock() + + @pytest.fixture + def coordinator(self): + yield MagicMock() + + @pytest.fixture + def device(self): + yield Shutter( + "id", + "name", + DeviceType.RAFFSTORE_90, + DeviceMode(DeviceType.RAFFSTORE_90), + list(Action), + ) + + @pytest.fixture + def entity(self, api, coordinator): + yield OnyxSensorDeviceType( + api, "UTC", coordinator, "name", DeviceType.RAFFSTORE_90, "uuid" + ) + + def test_icon(self, entity): + assert entity.icon == "mdi:cellphone-link" + + def test_name(self, entity): + assert entity.name == "name Device Type" + + def test_unique_id(self, entity): + assert entity.unique_id == "uuid/DeviceType" + + def test_state(self, api, entity, device): + api.device.return_value = device + assert entity.state == DeviceType.RAFFSTORE_90.string() + assert api.device.called diff --git a/tests/test_onyx_entity.py b/tests/sensors/test_onyx_entity.py similarity index 91% rename from tests/test_onyx_entity.py rename to tests/sensors/test_onyx_entity.py index 89e4167..bcb4232 100644 --- a/tests/test_onyx_entity.py +++ b/tests/sensors/test_onyx_entity.py @@ -9,7 +9,7 @@ from onyx_client.enum.device_type import DeviceType from custom_components.hella_onyx import DOMAIN -from custom_components.hella_onyx.onyx_entity import OnyxEntity +from custom_components.hella_onyx.sensors.onyx_entity import OnyxEntity class TestOnyxEntity: @@ -28,7 +28,7 @@ def entity(self, api, coordinator): ) def test_icon(self, entity): - assert entity.icon == "mdi:window-shutter" + assert entity.icon == "mdi:help" def test_device_info(self, entity): assert entity.device_info == { diff --git a/tests/sensors/test_shutter.py b/tests/sensors/test_shutter.py new file mode 100644 index 0000000..ba5a390 --- /dev/null +++ b/tests/sensors/test_shutter.py @@ -0,0 +1,424 @@ +"""Test for the ONYX Shutter Entity.""" +import time +from datetime import datetime +from unittest.mock import MagicMock, patch + +import pytest +import pytz +from homeassistant.components.cover import ( + CoverDeviceClass, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, + SUPPORT_STOP, +) +from homeassistant.core import HomeAssistant +from onyx_client.data.animation_keyframe import AnimationKeyframe +from onyx_client.data.animation_value import AnimationValue +from onyx_client.data.device_mode import DeviceMode +from onyx_client.data.numeric_value import NumericValue +from onyx_client.device.shutter import Shutter +from onyx_client.enum.action import Action +from onyx_client.enum.device_type import DeviceType + +from custom_components.hella_onyx.cover import OnyxShutter +from custom_components.hella_onyx.enum.moving_state import MovingState + + +class TestOnyxShutter: + @pytest.fixture + def api(self): + yield MagicMock() + + @pytest.fixture + def coordinator(self): + yield MagicMock() + + @pytest.fixture + def hass(self): + hass = MagicMock(spec=HomeAssistant) + hass.loop = MagicMock() + yield hass + + @pytest.fixture + def entity(self, api, coordinator, hass): + shutter = OnyxShutter( + api, "UTC", coordinator, "name", DeviceType.RAFFSTORE_90, "uuid" + ) + shutter.hass = hass + yield shutter + + @pytest.fixture + def rollershutter_entity(self, api, coordinator): + yield OnyxShutter( + api, "UTC", coordinator, "name", DeviceType.ROLLERSHUTTER, "uuid" + ) + + @pytest.fixture + def device(self): + yield Shutter( + "id", + "name", + DeviceType.RAFFSTORE_90, + DeviceMode(DeviceType.RAFFSTORE_90), + list(Action), + ) + + def test_icon(self, entity): + assert entity.icon == "mdi:window-shutter" + + def test_name(self, entity): + assert entity.name == "name" + + def test_unique_id(self, entity): + assert entity.unique_id == "uuid/Shutter" + + def test_device_class(self, entity): + assert entity.device_class == CoverDeviceClass.SHUTTER + + def test_supported_features_with_tilt(self, entity): + assert entity.supported_features == ( + SUPPORT_OPEN + | SUPPORT_CLOSE + | SUPPORT_STOP + | SUPPORT_SET_POSITION + | SUPPORT_SET_TILT_POSITION + ) + + def test_supported_features_without_tilt(self, rollershutter_entity): + assert rollershutter_entity.supported_features == ( + SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION + ) + + def test_current_cover_position(self, api, entity, device): + device.actual_position = NumericValue( + value=10, minimum=0, maximum=100, read_only=False + ) + api.device.return_value = device + assert entity.current_cover_position == 90 + assert api.device.called + + def test_current_cover_position_with_animation(self, api, entity, device): + animation = AnimationValue( + start=0, + current_value=0, + keyframes=[ + AnimationKeyframe( + interpolation="linear", duration=10, delay=0, value=10 + ) + ], + ) + device.actual_position = NumericValue( + value=10, minimum=0, maximum=100, read_only=False, animation=animation + ) + api.device.return_value = device + with patch.object(entity, "_start_moving_device") as mock_start_moving_device: + assert entity.current_cover_position == 90 + mock_start_moving_device.assert_called_with(animation) + assert api.device.called + + def test_current_cover_tilt_position(self, api, entity, device): + device.actual_angle = NumericValue( + value=10, minimum=0, maximum=100, read_only=False + ) + api.device.return_value = device + assert entity.current_cover_tilt_position == 11 + assert api.device.called + + def test_current_cover_tilt_position_with_animation(self, api, entity, device): + animation = AnimationValue( + start=0, + current_value=0, + keyframes=[ + AnimationKeyframe( + interpolation="linear", duration=10, delay=0, value=10 + ) + ], + ) + device.actual_angle = NumericValue( + value=10, minimum=0, maximum=100, read_only=False, animation=animation + ) + api.device.return_value = device + with patch.object(entity, "_start_moving_device") as mock_start_moving_device: + assert entity.current_cover_tilt_position == 11 + mock_start_moving_device.assert_called_with(animation) + assert api.device.called + + def test_is_not_opening(self, entity): + assert not entity.is_opening + + def test_is_opening(self, entity): + entity._moving_state = MovingState.OPENING + assert entity.is_opening + + def test_is_not_closing(self, entity): + assert not entity.is_closing + + def test_is_closing(self, entity): + entity._moving_state = MovingState.CLOSING + assert entity.is_closing + + def test_is_not_closed(self, api, entity, device): + device.actual_position = NumericValue( + value=10, maximum=100, minimum=0, read_only=False + ) + api.device.return_value = device + assert not entity.is_closed + assert api.device.called + + def test_is_closed(self, api, entity, device): + device.actual_position = NumericValue( + value=100, maximum=100, minimum=0, read_only=False + ) + api.device.return_value = device + assert entity.is_closed + assert api.device.called + + def test_start_moving_device_end(self, api, entity, device): + current_time = time.mktime(datetime.now(pytz.timezone("UTC")).timetuple()) + animation = AnimationValue( + start=current_time - 100, + current_value=0, + keyframes=[ + AnimationKeyframe( + interpolation="linear", + value=0, + duration=10, + delay=0, + ) + ], + ) + entity._moving_state = MovingState.CLOSING + with patch.object(entity, "_end_moving_device") as mock_end_moving_device: + entity._start_moving_device(animation) + mock_end_moving_device.assert_called() + + def test_start_moving_device_still(self, api, entity, device): + current_time = time.mktime(datetime.now(pytz.timezone("UTC")).timetuple()) + animation = AnimationValue( + start=current_time - 100, + current_value=0, + keyframes=[ + AnimationKeyframe( + interpolation="linear", + value=0, + duration=10, + delay=0, + ) + ], + ) + entity._moving_state = MovingState.STILL + with patch.object(entity, "_end_moving_device") as mock_end_moving_device: + entity._start_moving_device(animation) + mock_end_moving_device.assert_not_called() + + @patch("asyncio.run_coroutine_threadsafe") + def test_open_cover(self, mock_run_coroutine_threadsafe, api, entity, device): + device.actual_position = NumericValue( + value=100, maximum=100, minimum=0, read_only=False + ) + device.drivetime_up = NumericValue( + value=0, maximum=100, minimum=0, read_only=False + ) + api.device.return_value = device + with patch.object(entity, "_set_state") as mock_set_state: + entity.open_cover() + mock_set_state.assert_called_with(MovingState.OPENING) + api.send_device_command_action.assert_called_with("uuid", Action.OPEN) + assert mock_run_coroutine_threadsafe.called + + @patch("asyncio.run_coroutine_threadsafe") + def test_close_cover(self, mock_run_coroutine_threadsafe, api, entity, device): + device.actual_position = NumericValue( + value=100, maximum=100, minimum=0, read_only=False + ) + device.drivetime_down = NumericValue( + value=0, maximum=100, minimum=0, read_only=False + ) + api.device.return_value = device + with patch.object(entity, "_set_state") as mock_set_state: + entity.close_cover() + mock_set_state.assert_called_with(MovingState.CLOSING) + api.send_device_command_action.assert_called_with("uuid", Action.CLOSE) + assert mock_run_coroutine_threadsafe.called + + @patch("asyncio.run_coroutine_threadsafe") + def test_set_cover_position( + self, mock_run_coroutine_threadsafe, api, entity, device + ): + device.target_position = NumericValue( + value=100, maximum=100, minimum=0, read_only=False + ) + device.actual_position = NumericValue( + value=10, maximum=100, minimum=0, read_only=False + ) + device.target_angle = NumericValue( + value=30, maximum=100, minimum=0, read_only=False + ) + api.device.return_value = device + with patch.object( + entity, "_calculate_and_set_state" + ) as mock_calculate_and_set_state: + entity.set_cover_position(position=10) + mock_calculate_and_set_state.assert_called_with(10, 90) + api.send_device_command_properties.assert_called_with( + "uuid", + { + "target_position": 90, + "target_angle": 30, + }, + ) + assert api.device.called + assert mock_run_coroutine_threadsafe.called + + @patch("asyncio.run_coroutine_threadsafe") + def test_stop_cover(self, mock_run_coroutine_threadsafe, api, entity): + with patch.object(entity, "_set_state") as mock_set_state: + entity.stop_cover() + mock_set_state.assert_called_with(MovingState.STILL) + api.send_device_command_action.assert_called_with("uuid", Action.STOP) + assert mock_run_coroutine_threadsafe.called + + def test_open_cover_tilt(self, entity): + with pytest.raises(NotImplementedError): + entity.open_cover_tilt() + + def test_close_cover_tilt(self, entity): + with pytest.raises(NotImplementedError): + entity.close_cover_tilt() + + @patch("asyncio.run_coroutine_threadsafe") + def test_set_cover_tilt_position( + self, mock_run_coroutine_threadsafe, api, entity, device + ): + device.rotationtime = NumericValue( + value=100, maximum=100, minimum=0, read_only=False + ) + device.actual_angle = NumericValue( + value=10, maximum=100, minimum=0, read_only=False + ) + api.device.return_value = device + with patch.object( + entity, "_calculate_and_set_state" + ) as mock_calculate_and_set_state: + entity.set_cover_tilt_position(tilt_position=10) + mock_calculate_and_set_state.assert_called_with(10, 9) + api.send_device_command_properties.assert_called_with( + "uuid", {"target_angle": 9} + ) + assert api.device.called + assert mock_run_coroutine_threadsafe.called + + @patch("asyncio.run_coroutine_threadsafe") + def test_stop_cover_tilt(self, mock_run_coroutine_threadsafe, api, entity): + with patch.object(entity, "_set_state") as mock_set_state: + entity.stop_cover_tilt() + mock_set_state.assert_called_with(MovingState.STILL) + api.send_device_command_action.assert_called_with("uuid", Action.STOP) + assert mock_run_coroutine_threadsafe.called + + def test__set_state_STILL(self, entity): + with patch.object(entity, "async_update") as mock_async_update: + entity._set_state(MovingState.STILL) + assert not mock_async_update.called + assert not entity.is_opening + assert not entity.is_closing + + def test__set_state_CLOSING(self, entity): + entity._set_state(MovingState.CLOSING) + assert not entity.is_opening + assert entity.is_closing + + def test__set_state_OPENING(self, entity): + entity._set_state(MovingState.OPENING) + assert entity.is_opening + assert not entity.is_closing + + def test__end_moving_device(self, entity): + entity._moving_state = MovingState.CLOSING + entity._device.actual_angle.animation = AnimationValue( + time.time() - 1000, 10, [AnimationKeyframe("linear", 0, 100, 90)] + ) + with patch.object(entity, "stop_cover") as mock_stop_cover: + entity._end_moving_device() + assert mock_stop_cover.called + + def test__end_moving_device_within_time(self, entity): + entity._moving_state = MovingState.CLOSING + entity._device.actual_angle.animation = AnimationValue( + time.time(), 10, [AnimationKeyframe("linear", 0, 20000, 90)] + ) + entity._device.actual_position.animation = AnimationValue( + time.time(), 10, [AnimationKeyframe("linear", 0, 10000, 90)] + ) + with patch.object(entity, "stop_cover") as mock_stop_cover: + entity._end_moving_device() + assert not mock_stop_cover.called + + def test__end_moving_device_within_time_using_delay(self, entity): + entity._moving_state = MovingState.CLOSING + entity._device.actual_position.animation = AnimationValue( + time.time() - 100, 10, [AnimationKeyframe("linear", 100000, 10, 90)] + ) + with patch.object(entity, "stop_cover") as mock_stop_cover: + entity._end_moving_device() + assert not mock_stop_cover.called + + def test__end_moving_device_still(self, entity): + with patch.object(entity, "stop_cover") as mock_stop_cover: + entity._end_moving_device() + assert not mock_stop_cover.called + + def test__calculate_and_set_state_CLOSING(self, entity, device, api): + device.drivetime_down = NumericValue( + value=50, maximum=100, minimum=0, read_only=False + ) + api.device.return_value = device + with patch.object(entity, "_set_state") as mock_set_state: + with patch.object(entity, "_calculate_state") as mock_calculate_state: + mock_calculate_state.return_value = MovingState.CLOSING + entity._calculate_and_set_state(10, 100) + mock_calculate_state.assert_called_once_with(10, 100) + mock_set_state.assert_called_once_with(MovingState.CLOSING) + + def test__calculate_and_set_state_OPENING(self, entity, device, api): + device.drivetime_up = NumericValue( + value=50, maximum=100, minimum=0, read_only=False + ) + api.device.return_value = device + with patch.object(entity, "_set_state") as mock_set_state: + with patch.object(entity, "_calculate_state") as mock_calculate_state: + mock_calculate_state.return_value = MovingState.OPENING + entity._calculate_and_set_state(100, 10) + mock_calculate_state.assert_called_once_with(100, 10) + mock_set_state.assert_called_once_with(MovingState.OPENING) + + def test__calculate_and_set_state_tilt(self, entity): + with patch.object(entity, "_set_state") as mock_set_state: + with patch.object(entity, "_calculate_state") as mock_calculate_state: + mock_calculate_state.return_value = MovingState.CLOSING + entity._calculate_and_set_state(10, 100) + mock_calculate_state.assert_called_once_with(10, 100) + mock_set_state.assert_called_once_with(MovingState.CLOSING) + + def test__max_angle(self, entity): + assert entity._max_angle == 90 + + def test__max_angle_180(self, entity): + entity._type = DeviceType.RAFFSTORE_180 + assert entity._max_angle == 180 + + def test__max_angle_rollershutter(self, rollershutter_entity): + assert rollershutter_entity._max_angle == 100 + + def test__calculate_state_CLOSING(self, entity, device, api): + assert entity._calculate_state(100, 10) == MovingState.OPENING + + def test__calculate_state_OPENING(self, entity, device, api): + assert entity._calculate_state(10, 100) == MovingState.CLOSING + + def test__calculate_state_STILL(self, entity, api): + api.device.return_value = None + assert entity._calculate_state(10, 10) == MovingState.STILL + assert not api.device.called diff --git a/tests/sensors/test_weather.py b/tests/sensors/test_weather.py new file mode 100644 index 0000000..ffba870 --- /dev/null +++ b/tests/sensors/test_weather.py @@ -0,0 +1,379 @@ +"""Test for the ONYX Weather Sensors.""" + +from unittest.mock import MagicMock + +import pytest +from onyx_client.data.device_mode import DeviceMode +from onyx_client.data.numeric_value import NumericValue +from onyx_client.device.weather import Weather +from onyx_client.enum.action import Action +from onyx_client.enum.device_type import DeviceType + +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.const import ( + UnitOfTemperature, + UnitOfPressure, + UnitOfSpeed, + LIGHT_LUX, + PERCENTAGE, +) + +from custom_components.hella_onyx.sensors.weather import ( + OnyxSensorWeatherHumidity, + OnyxSensorWeatherTemperature, + OnyxSensorWeatherAirPressure, + OnyxSensorWeatherWindPeak, + OnyxSensorWeatherSunBrightnessPeak, + OnyxSensorWeatherSunBrightnessSink, +) + + +class TestOnyxSensorWeatherHumidity: + @pytest.fixture + def api(self): + yield MagicMock() + + @pytest.fixture + def coordinator(self): + yield MagicMock() + + @pytest.fixture + def hass(self): + yield MagicMock() + + @pytest.fixture + def device(self): + yield Weather( + "id", + "name", + DeviceType.WEATHER, + DeviceMode(DeviceType.WEATHER), + list(Action), + NumericValue(0, 0, 1, True), # wind peak + NumericValue(1, 1, 2, True), # sun brightness peak + NumericValue(2, 2, 3, True), # sun brightness sink + NumericValue(3, 3, 4, True), # air pressure + NumericValue(4, 4, 5, True), # humidity + NumericValue(5, 5, 6, True), # temperature + ) + + @pytest.fixture + def entity(self, api, coordinator, hass): + sensor = OnyxSensorWeatherHumidity( + api, "UTC", coordinator, "name", DeviceType.WEATHER, "uuid" + ) + sensor.hass = hass + yield sensor + + def test_icon(self, entity): + assert entity.icon == "mdi:water-percent" + + def test_name(self, entity): + assert entity.name == "name Humidity" + + def test_unique_id(self, entity): + assert entity.unique_id == "uuid/Humidity" + + def test_device_class(self, entity): + assert entity.device_class == SensorDeviceClass.HUMIDITY + + def test_unit_of_measurement(self, entity): + assert entity.unit_of_measurement == PERCENTAGE + + def test_state(self, api, entity, device): + api.device.return_value = device + assert entity.state == 4 + assert api.device.called + + +class TestOnyxSensorWeatherTemperature: + @pytest.fixture + def api(self): + yield MagicMock() + + @pytest.fixture + def coordinator(self): + yield MagicMock() + + @pytest.fixture + def hass(self): + yield MagicMock() + + @pytest.fixture + def device(self): + yield Weather( + "id", + "name", + DeviceType.WEATHER, + DeviceMode(DeviceType.WEATHER), + list(Action), + NumericValue(0, 0, 1, True), # wind peak + NumericValue(1, 1, 2, True), # sun brightness peak + NumericValue(2, 2, 3, True), # sun brightness sink + NumericValue(3, 3, 4, True), # air pressure + NumericValue(4, 4, 5, True), # humidity + NumericValue(50, 50, 60, True), # temperature + ) + + @pytest.fixture + def entity(self, api, coordinator, hass): + sensor = OnyxSensorWeatherTemperature( + api, "UTC", coordinator, "name", DeviceType.WEATHER, "uuid" + ) + sensor.hass = hass + yield sensor + + def test_icon(self, entity): + assert entity.icon == "mdi:thermometer" + + def test_name(self, entity): + assert entity.name == "name Temperature" + + def test_unique_id(self, entity): + assert entity.unique_id == "uuid/Temperature" + + def test_device_class(self, entity): + assert entity.device_class == SensorDeviceClass.TEMPERATURE + + def test_unit_of_measurement(self, entity, hass): + hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS + assert entity.unit_of_measurement == UnitOfTemperature.CELSIUS + assert entity.native_unit_of_measurement == UnitOfTemperature.CELSIUS + + def test_state(self, api, entity, device, hass): + hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS + api.device.return_value = device + assert entity.state == 5.0 + assert api.device.called + + +class TestOnyxSensorWeatherAirPressure: + @pytest.fixture + def api(self): + yield MagicMock() + + @pytest.fixture + def coordinator(self): + yield MagicMock() + + @pytest.fixture + def hass(self): + yield MagicMock() + + @pytest.fixture + def device(self): + yield Weather( + "id", + "name", + DeviceType.WEATHER, + DeviceMode(DeviceType.WEATHER), + list(Action), + NumericValue(0, 0, 1, True), # wind peak + NumericValue(1, 1, 2, True), # sun brightness peak + NumericValue(2, 2, 3, True), # sun brightness sink + NumericValue(300, 300, 400, True), # air pressure + NumericValue(4, 4, 5, True), # humidity + NumericValue(50, 50, 60, True), # temperature + ) + + @pytest.fixture + def entity(self, api, coordinator, hass): + sensor = OnyxSensorWeatherAirPressure( + api, "UTC", coordinator, "name", DeviceType.WEATHER, "uuid" + ) + sensor.hass = hass + yield sensor + + def test_icon(self, entity): + assert entity.icon == "mdi:gauge" + + def test_name(self, entity): + assert entity.name == "name Air Pressure" + + def test_unique_id(self, entity): + assert entity.unique_id == "uuid/AirPressure" + + def test_device_class(self, entity): + assert entity.device_class == SensorDeviceClass.ATMOSPHERIC_PRESSURE + + def test_unit_of_measurement(self, entity): + assert entity.unit_of_measurement == UnitOfPressure.HPA + + def test_state(self, api, entity, device): + api.device.return_value = device + assert entity.state == 3.0 + assert api.device.called + + +class TestOnyxSensorWeatherWindPeak: + @pytest.fixture + def api(self): + yield MagicMock() + + @pytest.fixture + def coordinator(self): + yield MagicMock() + + @pytest.fixture + def hass(self): + yield MagicMock() + + @pytest.fixture + def device(self): + yield Weather( + "id", + "name", + DeviceType.WEATHER, + DeviceMode(DeviceType.WEATHER), + list(Action), + NumericValue(6000, 6000, 7000, True), # wind peak + NumericValue(1, 1, 2, True), # sun brightness peak + NumericValue(2, 2, 3, True), # sun brightness sink + NumericValue(300, 300, 400, True), # air pressure + NumericValue(4, 4, 5, True), # humidity + NumericValue(50, 50, 60, True), # temperature + ) + + @pytest.fixture + def entity(self, api, coordinator, hass): + sensor = OnyxSensorWeatherWindPeak( + api, "UTC", coordinator, "name", DeviceType.WEATHER, "uuid" + ) + sensor.hass = hass + yield sensor + + def test_icon(self, entity): + assert entity.icon == "mdi:weather-windy" + + def test_name(self, entity): + assert entity.name == "name Wind Peak" + + def test_unique_id(self, entity): + assert entity.unique_id == "uuid/WindPeak" + + def test_device_class(self, entity): + assert entity.device_class == SensorDeviceClass.WIND_SPEED + + def test_unit_of_measurement(self, entity): + assert entity.unit_of_measurement == UnitOfSpeed.METERS_PER_SECOND + + def test_state(self, api, entity, device): + api.device.return_value = device + assert entity.state == 6.0 + assert api.device.called + + +class TestOnyxSensorWeatherSunBrightnessPeak: + @pytest.fixture + def api(self): + yield MagicMock() + + @pytest.fixture + def coordinator(self): + yield MagicMock() + + @pytest.fixture + def hass(self): + yield MagicMock() + + @pytest.fixture + def device(self): + yield Weather( + "id", + "name", + DeviceType.WEATHER, + DeviceMode(DeviceType.WEATHER), + list(Action), + NumericValue(6000, 6000, 7000, True), # wind peak + NumericValue(1, 1, 2, True), # sun brightness peak + NumericValue(2, 2, 3, True), # sun brightness sink + NumericValue(300, 300, 400, True), # air pressure + NumericValue(4, 4, 5, True), # humidity + NumericValue(50, 50, 60, True), # temperature + ) + + @pytest.fixture + def entity(self, api, coordinator, hass): + sensor = OnyxSensorWeatherSunBrightnessPeak( + api, "UTC", coordinator, "name", DeviceType.WEATHER, "uuid" + ) + sensor.hass = hass + yield sensor + + def test_icon(self, entity): + assert entity.icon == "mdi:weather-sunset-up" + + def test_name(self, entity): + assert entity.name == "name Sun Brightness Peak" + + def test_unique_id(self, entity): + assert entity.unique_id == "uuid/SunBrightnessPeak" + + def test_device_class(self, entity): + assert entity.device_class == SensorDeviceClass.ILLUMINANCE + + def test_unit_of_measurement(self, entity): + assert entity.unit_of_measurement == LIGHT_LUX + + def test_state(self, api, entity, device): + api.device.return_value = device + assert entity.state == 1 + assert api.device.called + + +class TestOnyxSensorWeatherSunBrightnessSink: + @pytest.fixture + def api(self): + yield MagicMock() + + @pytest.fixture + def coordinator(self): + yield MagicMock() + + @pytest.fixture + def hass(self): + yield MagicMock() + + @pytest.fixture + def device(self): + yield Weather( + "id", + "name", + DeviceType.WEATHER, + DeviceMode(DeviceType.WEATHER), + list(Action), + NumericValue(6000, 6000, 7000, True), # wind peak + NumericValue(1, 1, 2, True), # sun brightness peak + NumericValue(2, 2, 3, True), # sun brightness sink + NumericValue(300, 300, 400, True), # air pressure + NumericValue(4, 4, 5, True), # humidity + NumericValue(50, 50, 60, True), # temperature + ) + + @pytest.fixture + def entity(self, api, coordinator, hass): + sensor = OnyxSensorWeatherSunBrightnessSink( + api, "UTC", coordinator, "name", DeviceType.WEATHER, "uuid" + ) + sensor.hass = hass + yield sensor + + def test_icon(self, entity): + assert entity.icon == "mdi:weather-sunset-down" + + def test_name(self, entity): + assert entity.name == "name Sun Brightness Sink" + + def test_unique_id(self, entity): + assert entity.unique_id == "uuid/SunBrightnessSink" + + def test_device_class(self, entity): + assert entity.device_class == SensorDeviceClass.ILLUMINANCE + + def test_unit_of_measurement(self, entity): + assert entity.unit_of_measurement == LIGHT_LUX + + def test_state(self, api, entity, device): + api.device.return_value = device + assert entity.state == 2 + assert api.device.called diff --git a/tests/test_cover.py b/tests/test_cover.py index 5dc7bec..d8d1932 100644 --- a/tests/test_cover.py +++ b/tests/test_cover.py @@ -1,27 +1,11 @@ """Test for the ONYX Shutter Entity.""" -import time -from datetime import datetime from unittest.mock import MagicMock, patch import pytest -import pytz -from homeassistant.components.cover import ( - CoverDeviceClass, - SUPPORT_CLOSE, - SUPPORT_OPEN, - SUPPORT_SET_POSITION, - SUPPORT_SET_TILT_POSITION, - SUPPORT_STOP, -) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from onyx_client.data.animation_keyframe import AnimationKeyframe -from onyx_client.data.animation_value import AnimationValue from onyx_client.data.device_mode import DeviceMode -from onyx_client.data.numeric_value import NumericValue from onyx_client.device.light import Light from onyx_client.device.shutter import Shutter -from onyx_client.enum.action import Action from onyx_client.enum.device_type import DeviceType from custom_components.hella_onyx import ( @@ -30,8 +14,7 @@ ONYX_COORDINATOR, ONYX_TIMEZONE, ) -from custom_components.hella_onyx.cover import OnyxShutter, async_setup_entry -from custom_components.hella_onyx.moving_state import MovingState +from custom_components.hella_onyx.cover import async_setup_entry @patch("homeassistant.core.HomeAssistant") @@ -102,401 +85,6 @@ async def test_async_setup_entry_filter_all(mock_hass): assert len(async_add_entries.data) == 0 -class TestOnyxShutter: - @pytest.fixture - def api(self): - yield MagicMock() - - @pytest.fixture - def coordinator(self): - yield MagicMock() - - @pytest.fixture - def hass(self): - hass = MagicMock(spec=HomeAssistant) - hass.loop = MagicMock() - yield hass - - @pytest.fixture - def entity(self, api, coordinator, hass): - shutter = OnyxShutter( - api, "UTC", coordinator, "name", DeviceType.RAFFSTORE_90, "uuid" - ) - shutter.hass = hass - yield shutter - - @pytest.fixture - def rollershutter_entity(self, api, coordinator): - yield OnyxShutter( - api, "UTC", coordinator, "name", DeviceType.ROLLERSHUTTER, "uuid" - ) - - @pytest.fixture - def device(self): - yield Shutter( - "id", - "name", - DeviceType.RAFFSTORE_90, - DeviceMode(DeviceType.RAFFSTORE_90), - list(Action), - ) - - def test_name(self, entity): - assert entity.name == "name" - - def test_unique_id(self, entity): - assert entity.unique_id == "uuid/Shutter" - - def test_device_class(self, entity): - assert entity.device_class == CoverDeviceClass.SHUTTER - - def test_supported_features_with_tilt(self, entity): - assert entity.supported_features == ( - SUPPORT_OPEN - | SUPPORT_CLOSE - | SUPPORT_STOP - | SUPPORT_SET_POSITION - | SUPPORT_SET_TILT_POSITION - ) - - def test_supported_features_without_tilt(self, rollershutter_entity): - assert rollershutter_entity.supported_features == ( - SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION - ) - - def test_current_cover_position(self, api, entity, device): - device.actual_position = NumericValue( - value=10, minimum=0, maximum=100, read_only=False - ) - api.device.return_value = device - assert entity.current_cover_position == 90 - assert api.device.called - - def test_current_cover_position_with_animation(self, api, entity, device): - animation = AnimationValue( - start=0, - current_value=0, - keyframes=[ - AnimationKeyframe( - interpolation="linear", duration=10, delay=0, value=10 - ) - ], - ) - device.actual_position = NumericValue( - value=10, minimum=0, maximum=100, read_only=False, animation=animation - ) - api.device.return_value = device - with patch.object(entity, "_start_moving_device") as mock_start_moving_device: - assert entity.current_cover_position == 90 - mock_start_moving_device.assert_called_with(animation) - assert api.device.called - - def test_current_cover_tilt_position(self, api, entity, device): - device.actual_angle = NumericValue( - value=10, minimum=0, maximum=100, read_only=False - ) - api.device.return_value = device - assert entity.current_cover_tilt_position == 11 - assert api.device.called - - def test_current_cover_tilt_position_with_animation(self, api, entity, device): - animation = AnimationValue( - start=0, - current_value=0, - keyframes=[ - AnimationKeyframe( - interpolation="linear", duration=10, delay=0, value=10 - ) - ], - ) - device.actual_angle = NumericValue( - value=10, minimum=0, maximum=100, read_only=False, animation=animation - ) - api.device.return_value = device - with patch.object(entity, "_start_moving_device") as mock_start_moving_device: - assert entity.current_cover_tilt_position == 11 - mock_start_moving_device.assert_called_with(animation) - assert api.device.called - - def test_is_not_opening(self, entity): - assert not entity.is_opening - - def test_is_opening(self, entity): - entity._moving_state = MovingState.OPENING - assert entity.is_opening - - def test_is_not_closing(self, entity): - assert not entity.is_closing - - def test_is_closing(self, entity): - entity._moving_state = MovingState.CLOSING - assert entity.is_closing - - def test_is_not_closed(self, api, entity, device): - device.actual_position = NumericValue( - value=10, maximum=100, minimum=0, read_only=False - ) - api.device.return_value = device - assert not entity.is_closed - assert api.device.called - - def test_is_closed(self, api, entity, device): - device.actual_position = NumericValue( - value=100, maximum=100, minimum=0, read_only=False - ) - api.device.return_value = device - assert entity.is_closed - assert api.device.called - - def test_start_moving_device_end(self, api, entity, device): - current_time = time.mktime(datetime.now(pytz.timezone("UTC")).timetuple()) - animation = AnimationValue( - start=current_time - 100, - current_value=0, - keyframes=[ - AnimationKeyframe( - interpolation="linear", - value=0, - duration=10, - delay=0, - ) - ], - ) - entity._moving_state = MovingState.CLOSING - with patch.object(entity, "_end_moving_device") as mock_end_moving_device: - entity._start_moving_device(animation) - mock_end_moving_device.assert_called() - - def test_start_moving_device_still(self, api, entity, device): - current_time = time.mktime(datetime.now(pytz.timezone("UTC")).timetuple()) - animation = AnimationValue( - start=current_time - 100, - current_value=0, - keyframes=[ - AnimationKeyframe( - interpolation="linear", - value=0, - duration=10, - delay=0, - ) - ], - ) - entity._moving_state = MovingState.STILL - with patch.object(entity, "_end_moving_device") as mock_end_moving_device: - entity._start_moving_device(animation) - mock_end_moving_device.assert_not_called() - - @patch("asyncio.run_coroutine_threadsafe") - def test_open_cover(self, mock_run_coroutine_threadsafe, api, entity, device): - device.actual_position = NumericValue( - value=100, maximum=100, minimum=0, read_only=False - ) - device.drivetime_up = NumericValue( - value=0, maximum=100, minimum=0, read_only=False - ) - api.device.return_value = device - with patch.object(entity, "_set_state") as mock_set_state: - entity.open_cover() - mock_set_state.assert_called_with(MovingState.OPENING) - api.send_device_command_action.assert_called_with("uuid", Action.OPEN) - assert mock_run_coroutine_threadsafe.called - - @patch("asyncio.run_coroutine_threadsafe") - def test_close_cover(self, mock_run_coroutine_threadsafe, api, entity, device): - device.actual_position = NumericValue( - value=100, maximum=100, minimum=0, read_only=False - ) - device.drivetime_down = NumericValue( - value=0, maximum=100, minimum=0, read_only=False - ) - api.device.return_value = device - with patch.object(entity, "_set_state") as mock_set_state: - entity.close_cover() - mock_set_state.assert_called_with(MovingState.CLOSING) - api.send_device_command_action.assert_called_with("uuid", Action.CLOSE) - assert mock_run_coroutine_threadsafe.called - - @patch("asyncio.run_coroutine_threadsafe") - def test_set_cover_position( - self, mock_run_coroutine_threadsafe, api, entity, device - ): - device.target_position = NumericValue( - value=100, maximum=100, minimum=0, read_only=False - ) - device.actual_position = NumericValue( - value=10, maximum=100, minimum=0, read_only=False - ) - device.target_angle = NumericValue( - value=30, maximum=100, minimum=0, read_only=False - ) - api.device.return_value = device - with patch.object( - entity, "_calculate_and_set_state" - ) as mock_calculate_and_set_state: - entity.set_cover_position(position=10) - mock_calculate_and_set_state.assert_called_with(10, 90) - api.send_device_command_properties.assert_called_with( - "uuid", - { - "target_position": 90, - "target_angle": 30, - }, - ) - assert api.device.called - assert mock_run_coroutine_threadsafe.called - - @patch("asyncio.run_coroutine_threadsafe") - def test_stop_cover(self, mock_run_coroutine_threadsafe, api, entity): - with patch.object(entity, "_set_state") as mock_set_state: - entity.stop_cover() - mock_set_state.assert_called_with(MovingState.STILL) - api.send_device_command_action.assert_called_with("uuid", Action.STOP) - assert mock_run_coroutine_threadsafe.called - - def test_open_cover_tilt(self, entity): - with pytest.raises(NotImplementedError): - entity.open_cover_tilt() - - def test_close_cover_tilt(self, entity): - with pytest.raises(NotImplementedError): - entity.close_cover_tilt() - - @patch("asyncio.run_coroutine_threadsafe") - def test_set_cover_tilt_position( - self, mock_run_coroutine_threadsafe, api, entity, device - ): - device.rotationtime = NumericValue( - value=100, maximum=100, minimum=0, read_only=False - ) - device.actual_angle = NumericValue( - value=10, maximum=100, minimum=0, read_only=False - ) - api.device.return_value = device - with patch.object( - entity, "_calculate_and_set_state" - ) as mock_calculate_and_set_state: - entity.set_cover_tilt_position(tilt_position=10) - mock_calculate_and_set_state.assert_called_with(10, 9) - api.send_device_command_properties.assert_called_with( - "uuid", {"target_angle": 9} - ) - assert api.device.called - assert mock_run_coroutine_threadsafe.called - - @patch("asyncio.run_coroutine_threadsafe") - def test_stop_cover_tilt(self, mock_run_coroutine_threadsafe, api, entity): - with patch.object(entity, "_set_state") as mock_set_state: - entity.stop_cover_tilt() - mock_set_state.assert_called_with(MovingState.STILL) - api.send_device_command_action.assert_called_with("uuid", Action.STOP) - assert mock_run_coroutine_threadsafe.called - - def test__set_state_STILL(self, entity): - with patch.object(entity, "async_update") as mock_async_update: - entity._set_state(MovingState.STILL) - assert not mock_async_update.called - assert not entity.is_opening - assert not entity.is_closing - - def test__set_state_CLOSING(self, entity): - entity._set_state(MovingState.CLOSING) - assert not entity.is_opening - assert entity.is_closing - - def test__set_state_OPENING(self, entity): - entity._set_state(MovingState.OPENING) - assert entity.is_opening - assert not entity.is_closing - - def test__end_moving_device(self, entity): - entity._moving_state = MovingState.CLOSING - entity._device.actual_angle.animation = AnimationValue( - time.time() - 1000, 10, [AnimationKeyframe("linear", 0, 100, 90)] - ) - with patch.object(entity, "stop_cover") as mock_stop_cover: - entity._end_moving_device() - assert mock_stop_cover.called - - def test__end_moving_device_within_time(self, entity): - entity._moving_state = MovingState.CLOSING - entity._device.actual_angle.animation = AnimationValue( - time.time(), 10, [AnimationKeyframe("linear", 0, 20000, 90)] - ) - entity._device.actual_position.animation = AnimationValue( - time.time(), 10, [AnimationKeyframe("linear", 0, 10000, 90)] - ) - with patch.object(entity, "stop_cover") as mock_stop_cover: - entity._end_moving_device() - assert not mock_stop_cover.called - - def test__end_moving_device_within_time_using_delay(self, entity): - entity._moving_state = MovingState.CLOSING - entity._device.actual_position.animation = AnimationValue( - time.time() - 100, 10, [AnimationKeyframe("linear", 100000, 10, 90)] - ) - with patch.object(entity, "stop_cover") as mock_stop_cover: - entity._end_moving_device() - assert not mock_stop_cover.called - - def test__end_moving_device_still(self, entity): - with patch.object(entity, "stop_cover") as mock_stop_cover: - entity._end_moving_device() - assert not mock_stop_cover.called - - def test__calculate_and_set_state_CLOSING(self, entity, device, api): - device.drivetime_down = NumericValue( - value=50, maximum=100, minimum=0, read_only=False - ) - api.device.return_value = device - with patch.object(entity, "_set_state") as mock_set_state: - with patch.object(entity, "_calculate_state") as mock_calculate_state: - mock_calculate_state.return_value = MovingState.CLOSING - entity._calculate_and_set_state(10, 100) - mock_calculate_state.assert_called_once_with(10, 100) - mock_set_state.assert_called_once_with(MovingState.CLOSING) - - def test__calculate_and_set_state_OPENING(self, entity, device, api): - device.drivetime_up = NumericValue( - value=50, maximum=100, minimum=0, read_only=False - ) - api.device.return_value = device - with patch.object(entity, "_set_state") as mock_set_state: - with patch.object(entity, "_calculate_state") as mock_calculate_state: - mock_calculate_state.return_value = MovingState.OPENING - entity._calculate_and_set_state(100, 10) - mock_calculate_state.assert_called_once_with(100, 10) - mock_set_state.assert_called_once_with(MovingState.OPENING) - - def test__calculate_and_set_state_tilt(self, entity): - with patch.object(entity, "_set_state") as mock_set_state: - with patch.object(entity, "_calculate_state") as mock_calculate_state: - mock_calculate_state.return_value = MovingState.CLOSING - entity._calculate_and_set_state(10, 100) - mock_calculate_state.assert_called_once_with(10, 100) - mock_set_state.assert_called_once_with(MovingState.CLOSING) - - def test__max_angle(self, entity): - assert entity._max_angle == 90 - - def test__max_angle_180(self, entity): - entity._type = DeviceType.RAFFSTORE_180 - assert entity._max_angle == 180 - - def test__max_angle_rollershutter(self, rollershutter_entity): - assert rollershutter_entity._max_angle == 100 - - def test__calculate_state_CLOSING(self, entity, device, api): - assert entity._calculate_state(100, 10) == MovingState.OPENING - - def test__calculate_state_OPENING(self, entity, device, api): - assert entity._calculate_state(10, 100) == MovingState.CLOSING - - def test__calculate_state_STILL(self, entity, api): - api.device.return_value = None - assert entity._calculate_state(10, 10) == MovingState.STILL - assert not api.device.called - - class AsyncAddEntries: def __init__(self): self.called_async_add_entities = False diff --git a/tests/test_sensor.py b/tests/test_sensor.py index 22e0ecc..36e2ef7 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -1,4 +1,4 @@ -"""Test for the LED-Pi Sensor Entities.""" +"""Test for the ONYX Sensors.""" from unittest.mock import MagicMock, patch @@ -6,8 +6,8 @@ from homeassistant.config_entries import ConfigEntry from onyx_client.data.device_mode import DeviceMode from onyx_client.device.light import Light +from onyx_client.device.weather import Weather from onyx_client.device.shutter import Shutter -from onyx_client.enum.action import Action from onyx_client.enum.device_type import DeviceType from custom_components.hella_onyx import ( @@ -16,10 +16,7 @@ ONYX_COORDINATOR, ONYX_TIMEZONE, ) -from custom_components.hella_onyx.sensor import ( - OnyxSensorDeviceType, - async_setup_entry, -) +from custom_components.hella_onyx.sensor import async_setup_entry @patch("homeassistant.core.HomeAssistant") @@ -42,6 +39,13 @@ async def test_async_setup_entry(mock_hass): DeviceMode(DeviceType.BASIC_LIGHT), list(), ), + "weather": Weather( + "weather", + "name", + DeviceType.WEATHER, + DeviceMode(DeviceType.WEATHER), + list(), + ), } async_add_entries = AsyncAddEntries() mock_hass.data = { @@ -56,8 +60,22 @@ async def test_async_setup_entry(mock_hass): await async_setup_entry(mock_hass, config_entry, async_add_entries.call) assert async_add_entries.called_async_add_entities - assert len(async_add_entries.data) == 1 + assert len(async_add_entries.data) == 8 assert async_add_entries.data[0]._uuid == "shutter" + assert async_add_entries.data[1]._uuid == "weather" + assert async_add_entries.data[1].unique_id == "weather/DeviceType" + assert async_add_entries.data[2]._uuid == "weather" + assert async_add_entries.data[2].unique_id == "weather/Humidity" + assert async_add_entries.data[3]._uuid == "weather" + assert async_add_entries.data[3].unique_id == "weather/Temperature" + assert async_add_entries.data[4]._uuid == "weather" + assert async_add_entries.data[4].unique_id == "weather/AirPressure" + assert async_add_entries.data[5]._uuid == "weather" + assert async_add_entries.data[5].unique_id == "weather/WindPeak" + assert async_add_entries.data[6]._uuid == "weather" + assert async_add_entries.data[6].unique_id == "weather/SunBrightnessPeak" + assert async_add_entries.data[7]._uuid == "weather" + assert async_add_entries.data[7].unique_id == "weather/SunBrightnessSink" @patch("homeassistant.core.HomeAssistant") @@ -90,46 +108,6 @@ async def test_async_setup_entry_filter_all(mock_hass): assert len(async_add_entries.data) == 0 -class TestOnyxSensorDeviceType: - @pytest.fixture - def api(self): - yield MagicMock() - - @pytest.fixture - def coordinator(self): - yield MagicMock() - - @pytest.fixture - def device(self): - yield Shutter( - "id", - "name", - DeviceType.RAFFSTORE_90, - DeviceMode(DeviceType.RAFFSTORE_90), - list(Action), - ) - - @pytest.fixture - def entity(self, api, coordinator): - yield OnyxSensorDeviceType( - api, "UTC", coordinator, "name", DeviceType.RAFFSTORE_90, "uuid" - ) - - def test_icon(self, entity): - assert entity.icon == "mdi:cellphone-link" - - def test_name(self, entity): - assert entity.name == "name Device Type" - - def test_unique_id(self, entity): - assert entity.unique_id == "uuid/DeviceType" - - def test_state(self, api, entity, device): - api.device.return_value = device - assert entity.state == DeviceType.RAFFSTORE_90.string() - assert api.device.called - - class AsyncAddEntries: def __init__(self): self.called_async_add_entities = False