diff --git a/custom_components/dyson_local/__init__.py b/custom_components/dyson_local/__init__.py index 83235c11e..21db41b33 100644 --- a/custom_components/dyson_local/__init__.py +++ b/custom_components/dyson_local/__init__.py @@ -9,6 +9,7 @@ from .vendor.libdyson import ( Dyson360Eye, Dyson360Heurist, + Dyson360VisNav, DysonPureHotCool, DysonPureHotCoolLink, DysonPurifierHumidifyCool, @@ -94,11 +95,8 @@ async def async_setup_account(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_ACCOUNT: account, DATA_DEVICES: devices, } - for component in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) - ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -113,7 +111,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_DEVICE_TYPE], ) - if not isinstance(device, Dyson360Eye) and not isinstance(device, Dyson360Heurist): + if (not isinstance(device, Dyson360Eye) + and not isinstance(device, Dyson360Heurist) + and not isinstance(device, Dyson360VisNav)): # Set up coordinator async def async_update_data(): """Poll environmental data from the device.""" @@ -132,12 +132,6 @@ async def async_update_data(): else: coordinator = None - async def _async_forward_entry_setup(): - for component in _async_get_platforms(device): - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) - ) - def setup_entry(host: str, is_discovery: bool = True) -> bool: try: device.connect(host) @@ -153,7 +147,7 @@ def setup_entry(host: str, is_discovery: bool = True) -> bool: hass.data[DOMAIN][DATA_DEVICES][entry.entry_id] = device hass.data[DOMAIN][DATA_COORDINATORS][entry.entry_id] = coordinator asyncio.run_coroutine_threadsafe( - _async_forward_entry_setup(), hass.loop + hass.config_entries.async_forward_entry_setups(entry, _async_get_platforms(device)), hass.loop ).result() host = entry.data.get(CONF_HOST) @@ -203,7 +197,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @callback def _async_get_platforms(device: DysonDevice) -> List[str]: - if isinstance(device, Dyson360Eye) or isinstance(device, Dyson360Heurist): + if (isinstance(device, Dyson360Eye) + or isinstance(device, Dyson360Heurist) + or isinstance(device, Dyson360VisNav)): return ["binary_sensor", "sensor", "vacuum"] platforms = ["fan", "select", "sensor", "switch"] if isinstance(device, DysonPureHotCool): diff --git a/custom_components/dyson_local/binary_sensor.py b/custom_components/dyson_local/binary_sensor.py index 72c007333..daa974c12 100644 --- a/custom_components/dyson_local/binary_sensor.py +++ b/custom_components/dyson_local/binary_sensor.py @@ -2,7 +2,12 @@ from typing import Callable -from .vendor.libdyson import Dyson360Eye, Dyson360Heurist, DysonPureHotCoolLink +from .vendor.libdyson import ( + Dyson360Eye, + Dyson360Heurist, + Dyson360VisNav, + DysonPureHotCoolLink, +) from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -29,6 +34,13 @@ async def async_setup_entry( if isinstance(device, Dyson360Eye): entities.append(DysonVacuumBatteryChargingSensor(device, name)) if isinstance(device, Dyson360Heurist): + entities.extend( + [ + DysonVacuumBatteryChargingSensor(device, name), + Dyson360VisNavBinFullSensor(device, name), + ] + ) + if isinstance(device, Dyson360VisNav): entities.extend( [ DysonVacuumBatteryChargingSensor(device, name), @@ -92,6 +104,32 @@ def sub_unique_id(self): return "bin_full" +class Dyson360VisNavBinFullSensor(DysonEntity, BinarySensorEntity): + """Dyson 360 VisNav bin full sensor.""" + + _attr_entity_category = EntityCategory.DIAGNOSTIC + + @property + def is_on(self) -> bool: + """Return if the sensor is on.""" + return self._device.is_bin_full + + @property + def icon(self) -> str: + """Return the sensor icon.""" + return ICON_BIN_FULL + + @property + def sub_name(self) -> str: + """Return the name of the sensor.""" + return "Bin Full" + + @property + def sub_unique_id(self): + """Return the sensor's unique id.""" + return "bin_full" + + class DysonPureHotCoolLinkTiltSensor(DysonEntity, BinarySensorEntity): """Dyson Pure Hot+Cool Link tilt sensor.""" diff --git a/custom_components/dyson_local/climate.py b/custom_components/dyson_local/climate.py index 4ad4bf4a9..7460d6e11 100644 --- a/custom_components/dyson_local/climate.py +++ b/custom_components/dyson_local/climate.py @@ -46,6 +46,8 @@ async def async_setup_entry( class DysonClimateEntity(DysonEntity, ClimateEntity): """Dyson climate entity base class.""" + _enable_turn_on_off_backwards_compatibility = False + @property def hvac_mode(self) -> str: """Return hvac operation.""" @@ -76,6 +78,12 @@ def supported_features(self) -> int: """Return the list of supported features.""" return SUPPORT_FLAGS + def turn_on(self) -> None: + self._device.turn_on() + + def turn_off(self) -> None: + self._device.turn_off() + @property def temperature_unit(self) -> str: """Return the unit of measurement.""" diff --git a/custom_components/dyson_local/fan.py b/custom_components/dyson_local/fan.py index 7bb3a27f8..fe139be2f 100644 --- a/custom_components/dyson_local/fan.py +++ b/custom_components/dyson_local/fan.py @@ -52,7 +52,13 @@ SPEED_RANGE = (1, 10) -COMMON_FEATURES = FanEntityFeature.OSCILLATE | FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE +COMMON_FEATURES = ( + FanEntityFeature.OSCILLATE + | FanEntityFeature.SET_SPEED + | FanEntityFeature.PRESET_MODE + | FanEntityFeature.TURN_ON + | FanEntityFeature.TURN_OFF +) async def async_setup_entry( @@ -82,6 +88,8 @@ async def async_setup_entry( class DysonFanEntity(DysonEntity, FanEntity): """Dyson fan entity base class.""" + _enable_turn_on_off_backwards_compatibility = False + _MESSAGE_TYPE = MessageType.STATE @property diff --git a/custom_components/dyson_local/manifest.json b/custom_components/dyson_local/manifest.json index 41e0e9252..7357afdc2 100644 --- a/custom_components/dyson_local/manifest.json +++ b/custom_components/dyson_local/manifest.json @@ -8,6 +8,6 @@ "import_executor": true, "iot_class": "local_push", "issue_tracker": "https://github.com/libdyson-wg/ha-dyson/issues", - "version": "1.3.11", + "version": "1.4.1", "import_executor": true } diff --git a/custom_components/dyson_local/sensor.py b/custom_components/dyson_local/sensor.py index 8c8e8d2b4..06d479dcb 100644 --- a/custom_components/dyson_local/sensor.py +++ b/custom_components/dyson_local/sensor.py @@ -5,6 +5,7 @@ from .vendor.libdyson import ( Dyson360Eye, Dyson360Heurist, + Dyson360VisNav, DysonDevice, DysonPureCoolLink, DysonPurifierHumidifyCool, @@ -44,7 +45,7 @@ async def async_setup_entry( """Set up Dyson sensor from a config entry.""" device = hass.data[DOMAIN][DATA_DEVICES][config_entry.entry_id] name = config_entry.data[CONF_NAME] - if isinstance(device, Dyson360Eye) or isinstance(device, Dyson360Heurist): + if isinstance(device, Dyson360Eye) or isinstance(device, Dyson360Heurist) or isinstance(device, Dyson360VisNav): entities = [DysonBatterySensor(device, name)] else: coordinator = hass.data[DOMAIN][DATA_COORDINATORS][config_entry.entry_id] diff --git a/custom_components/dyson_local/vacuum.py b/custom_components/dyson_local/vacuum.py index 289cbe03b..879f65cad 100644 --- a/custom_components/dyson_local/vacuum.py +++ b/custom_components/dyson_local/vacuum.py @@ -4,8 +4,10 @@ from .vendor.libdyson import ( Dyson360Eye, + Dyson360VisNav, VacuumEyePowerMode, VacuumHeuristPowerMode, + VacuumVisNavPowerMode, VacuumState, ) @@ -68,6 +70,7 @@ VacuumState.MAPPING_NEEDS_CHARGE: "Mapping - Needs charging", VacuumState.MAPPING_PAUSED: "Mapping - Paused", VacuumState.MAPPING_RUNNING: "Mapping - Running", + VacuumState.MACHINE_OFF: "Off", } DYSON_STATES = { @@ -103,6 +106,7 @@ VacuumState.MAPPING_NEEDS_CHARGE: STATE_RETURNING, VacuumState.MAPPING_PAUSED: STATE_PAUSED, VacuumState.MAPPING_RUNNING: STATE_CLEANING, + VacuumState.MACHINE_OFF: STATE_DOCKED, } EYE_POWER_MODE_ENUM_TO_STR = { @@ -120,6 +124,15 @@ HEURIST_POWER_MODE_STR_TO_ENUM = { value: key for key, value in HEURIST_POWER_MODE_ENUM_TO_STR.items() } +VIS_NAV_POWER_MODE_ENUM_TO_STR = { + VacuumVisNavPowerMode.AUTO: "Auto", + VacuumVisNavPowerMode.QUICK: "Quick", + VacuumVisNavPowerMode.QUIET: "Quiet", + VacuumVisNavPowerMode.BOOST: "Boost", +} +VIS_NAV_POWER_MODE_STR_TO_ENUM = { + value: key for key, value in VIS_NAV_POWER_MODE_ENUM_TO_STR.items() +} ATTR_POSITION = "position" @@ -132,6 +145,8 @@ async def async_setup_entry( name = config_entry.data[CONF_NAME] if isinstance(device, Dyson360Eye): entity = Dyson360EyeEntity(device, name) + elif isinstance(device, Dyson360VisNav): # Dyson360VisNav + entity = Dyson360VisNavEntity(device, name) else: # Dyson360Heurist entity = Dyson360HeuristEntity(device, name) async_add_entities([entity]) @@ -230,3 +245,21 @@ def start(self) -> None: def set_fan_speed(self, fan_speed: str, **kwargs) -> None: """Set fan speed.""" self._device.set_default_power_mode(HEURIST_POWER_MODE_STR_TO_ENUM[fan_speed]) + + +class Dyson360VisNavEntity(Dyson360HeuristEntity): + """Dyson 360 Vis Nav robot vacuum entity.""" + + @property + def fan_speed(self) -> str: + """Return the fan speed of the vacuum cleaner.""" + return VIS_NAV_POWER_MODE_ENUM_TO_STR[self._device.current_power_mode] + + @property + def fan_speed_list(self) -> List[str]: + """Get the list of available fan speed steps of the vacuum cleaner.""" + return list(VIS_NAV_POWER_MODE_STR_TO_ENUM.keys()) + + def set_fan_speed(self, fan_speed: str, **kwargs) -> None: + """Set fan speed.""" + self._device.set_default_power_mode(VIS_NAV_POWER_MODE_STR_TO_ENUM[fan_speed]) diff --git a/custom_components/dyson_local/vendor/libdyson/__init__.py b/custom_components/dyson_local/vendor/libdyson/__init__.py index 1618ff40b..605dfae75 100644 --- a/custom_components/dyson_local/vendor/libdyson/__init__.py +++ b/custom_components/dyson_local/vendor/libdyson/__init__.py @@ -4,6 +4,7 @@ from .const import ( DEVICE_TYPE_360_EYE, DEVICE_TYPE_360_HEURIST, + DEVICE_TYPE_360_VIS_NAV, DEVICE_TYPE_PURE_COOL, DEVICE_TYPE_PURIFIER_COOL_E, DEVICE_TYPE_PURIFIER_COOL_K, @@ -27,11 +28,13 @@ from .const import MessageType # noqa: F401 from .const import VacuumEyePowerMode # noqa: F401 from .const import VacuumHeuristPowerMode # noqa: F401 +from .const import VacuumVisNavPowerMode # noqa: F401 from .const import VacuumState # noqa: F401 from .const import WaterHardness # noqa: F401 from .discovery import DysonDiscovery # noqa: F401 from .dyson_360_eye import Dyson360Eye from .dyson_360_heurist import Dyson360Heurist +from .dyson_360_vis_nav import Dyson360VisNav from .dyson_device import DysonDevice from .dyson_pure_cool import DysonPureCool from .dyson_pure_cool_link import DysonPureCoolLink @@ -48,6 +51,8 @@ def get_device(serial: str, credential: str, device_type: str) -> Optional[Dyson return Dyson360Eye(serial, credential) if device_type == DEVICE_TYPE_360_HEURIST: return Dyson360Heurist(serial, credential) + if device_type == DEVICE_TYPE_360_VIS_NAV: + return Dyson360VisNav(serial, credential) if device_type in [ DEVICE_TYPE_PURE_COOL_LINK_DESK, DEVICE_TYPE_PURE_COOL_LINK, @@ -79,3 +84,4 @@ def get_device(serial: str, credential: str, device_type: str) -> Optional[Dyson }: return DysonBigQuiet(serial, credential, device_type) return None + diff --git a/custom_components/dyson_local/vendor/libdyson/cloud/account.py b/custom_components/dyson_local/vendor/libdyson/cloud/account.py index c2f0d0bff..4ce337818 100644 --- a/custom_components/dyson_local/vendor/libdyson/cloud/account.py +++ b/custom_components/dyson_local/vendor/libdyson/cloud/account.py @@ -35,6 +35,7 @@ FILE_PATH = pathlib.Path(__file__).parent.absolute() + class HTTPBearerAuth(AuthBase): """Attaches HTTP Bearder Authentication to the given Request object.""" diff --git a/custom_components/dyson_local/vendor/libdyson/const.py b/custom_components/dyson_local/vendor/libdyson/const.py index 259788854..a5b29cf67 100644 --- a/custom_components/dyson_local/vendor/libdyson/const.py +++ b/custom_components/dyson_local/vendor/libdyson/const.py @@ -3,6 +3,7 @@ DEVICE_TYPE_360_EYE = "N223" DEVICE_TYPE_360_HEURIST = "276" +DEVICE_TYPE_360_VIS_NAV = "277" DEVICE_TYPE_PURE_COOL_LINK_DESK = "469" # DP01? DP02? This one's a bit older, and scraping the Dyson website is unclear DEVICE_TYPE_PURE_COOL_DESK = "520" # AM06? This one's also a bit older, and also hard to scrape off the Dyson website DEVICE_TYPE_PURE_COOL_LINK = "475" # TP02 @@ -21,6 +22,7 @@ DEVICE_TYPE_NAMES = { DEVICE_TYPE_360_EYE: "360 Eye robot vacuum", DEVICE_TYPE_360_HEURIST: "360 Heurist robot vacuum", + DEVICE_TYPE_360_VIS_NAV: "360 Vis Nav robot vacuum", DEVICE_TYPE_PURE_COOL: "Pure Cool", DEVICE_TYPE_PURIFIER_COOL_K: "Purifier Cool", DEVICE_TYPE_PURIFIER_COOL_E: "Purifier Cool", @@ -111,6 +113,7 @@ class VacuumState(Enum): MAPPING_NEEDS_CHARGE = "MAPPING_NEEDS_CHARGE" MAPPING_PAUSED = "MAPPING_PAUSED" MAPPING_RUNNING = "MAPPING_RUNNING" + MACHINE_OFF = "MACHINE_OFF" class VacuumEyePowerMode(Enum): @@ -128,6 +131,15 @@ class VacuumHeuristPowerMode(Enum): MAX = "3" +class VacuumVisNavPowerMode(Enum): + """Dyson 360 Heurist power mode.""" + + AUTO = "1" + QUICK = "2" + QUIET = "3" + BOOST = "4" + + class CleaningType(Enum): """Vacuum cleaning type.""" diff --git a/custom_components/dyson_local/vendor/libdyson/dyson_pure_cool_link.py b/custom_components/dyson_local/vendor/libdyson/dyson_pure_cool_link.py index ecf78e413..95a3bdf4e 100644 --- a/custom_components/dyson_local/vendor/libdyson/dyson_pure_cool_link.py +++ b/custom_components/dyson_local/vendor/libdyson/dyson_pure_cool_link.py @@ -7,6 +7,10 @@ class DysonPureCoolLink(DysonFanDevice): """Dyson Pure Cool Link device.""" + def __init__(self, serial: str, credential: str, device_type: str): + super().__init__(serial, credential, device_type) + self.preset_mode = "FAN" + @property def fan_mode(self) -> str: """Return the fan mode of the fan.""" @@ -20,7 +24,11 @@ def is_on(self) -> bool: @property def auto_mode(self) -> bool: """Return auto mode status.""" - return self.fan_mode == "AUTO" + if not self.is_on: + return self.preset_mode == "AUTO" + else: + self.preset_mode = self.fan_mode + return self.preset_mode == "AUTO" @property def oscillation(self) -> bool: @@ -49,7 +57,7 @@ def volatile_organic_compounds(self) -> int: def turn_on(self) -> None: """Turn on the device.""" - self._set_configuration(fmod="FAN") + self._set_configuration(fmod=self.preset_mode) def turn_off(self) -> None: """Turn off the device.""" @@ -60,10 +68,12 @@ def _set_speed(self, speed: int) -> None: def enable_auto_mode(self) -> None: """Turn on auto mode.""" + self.preset_mode = "AUTO" self._set_configuration(fmod="AUTO") def disable_auto_mode(self) -> None: """Turn off auto mode.""" + self.preset_mode = "FAN" self._set_configuration(fmod="FAN") def enable_oscillation(self) -> None: diff --git a/script/sync-components b/script/sync-components index f96ae17c4..ed97203e2 100755 --- a/script/sync-components +++ b/script/sync-components @@ -46,7 +46,7 @@ rm -rf custom_components/ember_mug mv $TMPDIR/$$-ember/custom_components/ember_mug custom_components # renovate: datasource=github-releases depName=libdyson-wg/ha-dyson -DYSON_LOCAL_VERSION=v1.3.11 +DYSON_LOCAL_VERSION=v1.4.1 git clone --depth 1 --branch $DYSON_LOCAL_VERSION https://github.com/libdyson-wg/ha-dyson $TMPDIR/$$-dyson-local rm -rf custom_components/dyson_local mv $TMPDIR/$$-dyson-local/custom_components/dyson_local custom_components