Skip to content

Commit

Permalink
Merge pull request #165 from kongo09/AC0850x
Browse files Browse the repository at this point in the history
add new devices for the AC0850 and AC0950 families
  • Loading branch information
kongo09 authored Oct 25, 2024
2 parents fa719ce + 5b5e317 commit b1f5695
Show file tree
Hide file tree
Showing 14 changed files with 316 additions and 143 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@ Note: `configuration.yaml` is no longer supported and your configuration is not

## Supported models

- AC0850
- AC0850/11 AWS_Philips_AIR
- AC0850/11 AWS_Philips_AIR_Combo
- AC0850/20 AWS_Philips_AIR
- AC0850/20 AWS_Philips_AIR_Combo
- AC0850/31
- AC0950
- AC0951
- AC1214
- AC1715
- AC2729
Expand All @@ -73,6 +79,7 @@ Note: `configuration.yaml` is no longer supported and your configuration is not
- AC3039
- AC3055
- AC3059
- AC3421
- AC3259
- AC3737
- AC3829
Expand Down
24 changes: 16 additions & 8 deletions custom_components/philips_airpurifier_coap/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
"""Support for Philips AirPurifier with CoAP."""

from __future__ import annotations

import asyncio
from functools import partial
from ipaddress import IPv6Address, ip_address
import json
import logging
from os import path, walk
from os import walk
from pathlib import Path

from aioairctrl import CoAPClient
from getmac import get_mac_address
Expand All @@ -25,7 +27,6 @@
DATA_KEY_COORDINATOR,
DOMAIN,
ICONLIST_URL,
ICONS,
ICONS_PATH,
ICONS_URL,
LOADER_PATH,
Expand All @@ -37,7 +38,7 @@
_LOGGER = logging.getLogger(__name__)


PLATFORMS = ["fan", "binary_sensor", "sensor", "switch", "light", "select", "number"]
PLATFORMS = ["binary_sensor", "fan", "light", "number", "select", "sensor", "switch"]


# icons code thanks to Thomas Loven:
Expand All @@ -50,17 +51,20 @@ class ListingView(HomeAssistantView):
requires_auth = False

def __init__(self, url, iconpath) -> None:
"""Initialize the ListingView with a URL and icon path."""
self.url = url
self.iconpath = iconpath
self.name = "Icon Listing"

async def get(self, request):
"""Handle GET request to provide a JSON list of the used icons."""
icons = []
for (dirpath, dirnames, filenames) in walk(self.iconpath):
for dirpath, _dirnames, filenames in walk(self.iconpath):
icons.extend(
[
{"name": path.join(dirpath[len(self.iconpath):], fn[:-4])}
for fn in filenames if fn.endswith(".svg")
{"name": (Path(dirpath[len(self.iconpath) :]) / fn[:-4]).as_posix()}
for fn in filenames
if fn.endswith(".svg")
]
)
return json.dumps(icons)
Expand All @@ -70,12 +74,16 @@ async def async_setup(hass: HomeAssistant, config) -> bool:
"""Set up the icons for the Philips AirPurifier integration."""
_LOGGER.debug("async_setup called")

await hass.http.async_register_static_paths([StaticPathConfig(LOADER_URL, hass.config.path(LOADER_PATH), True)])
await hass.http.async_register_static_paths(
[StaticPathConfig(LOADER_URL, hass.config.path(LOADER_PATH), True)]
)
add_extra_js_url(hass, LOADER_URL)

iset = PAP
iconpath = hass.config.path(ICONS_PATH + "/" + iset)
await hass.http.async_register_static_paths([StaticPathConfig(ICONS_URL + "/" + iset, iconpath, True)])
await hass.http.async_register_static_paths(
[StaticPathConfig(ICONS_URL + "/" + iset, iconpath, True)]
)
hass.http.register_view(ListingView(ICONLIST_URL + "/" + iset, iconpath))

return True
Expand Down
18 changes: 9 additions & 9 deletions custom_components/philips_airpurifier_coap/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Philips Air Purifier & Humidifier Binary Sensors."""

from __future__ import annotations

from collections.abc import Callable
Expand Down Expand Up @@ -54,13 +55,11 @@ async def async_setup_entry( # noqa: D103
cls_available_binary_sensors = getattr(cls, "AVAILABLE_BINARY_SENSORS", [])
available_binary_sensors.extend(cls_available_binary_sensors)

binary_sensors = []

for binary_sensor in BINARY_SENSOR_TYPES:
if binary_sensor in status and binary_sensor in available_binary_sensors:
binary_sensors.append(
PhilipsBinarySensor(coordinator, name, model, binary_sensor)
)
binary_sensors = [
PhilipsBinarySensor(coordinator, name, model, binary_sensor)
for binary_sensor in BINARY_SENSOR_TYPES
if binary_sensor in status and binary_sensor in available_binary_sensors
]

async_add_entities(binary_sensors, update_before_add=False)

Expand Down Expand Up @@ -89,9 +88,10 @@ def __init__( # noqa: D107
try:
device_id = self._device_status[PhilipsApi.DEVICE_ID]
self._attr_unique_id = f"{self._model}-{device_id}-{kind.lower()}"
except Exception as e:
except KeyError as e:
_LOGGER.error("Failed retrieving unique_id: %s", e)
raise PlatformNotReady
raise PlatformNotReady from e

self._attrs: dict[str, Any] = {}
self.kind = kind

Expand Down
76 changes: 48 additions & 28 deletions custom_components/philips_airpurifier_coap/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""The Philips AirPurifier component."""
import asyncio

import ipaddress
import logging
import re
Expand Down Expand Up @@ -43,13 +43,13 @@ def __init__(self) -> None:
self._model: Any = None
self._name: Any = None
self._device_id: str = None
self._wifi_version: Any = None

def _get_schema(self, user_input):
"""Provide schema for user input."""
schema = vol.Schema(
return vol.Schema(
{vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): cv.string}
)
return schema

async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult:
"""Handle initial step of auto discovery flow."""
Expand Down Expand Up @@ -80,7 +80,7 @@ async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowRes
# get the status out of the queue
_LOGGER.debug("status for host %s is: %s", self._host, status)

except asyncio.TimeoutError:
except TimeoutError:
_LOGGER.warning(
r"Timeout, host %s looks like a Philips AirPurifier but doesn't answer, aborting",
self._host,
Expand Down Expand Up @@ -110,6 +110,9 @@ async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowRes
self._model = first_model[:9]
_LOGGER.debug("model type extracted: %s", self._model)

# autodetect Wifi version
self._wifi_version = status.get(PhilipsApi.WIFI_VERSION)

# autodetect name
self._name = list(
filter(
Expand All @@ -129,18 +132,21 @@ async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowRes
)

# check if model is supported
if self._model not in model_to_class:
_LOGGER.info(
"Model %s found, but not supported directly. Trying model family",
self._model,
)
self._model = self._model[:6]
if self._model not in model_to_class:
_LOGGER.warning(
"Model %s found, but not supported. Aborting discovery",
self._model,
)
return self.async_abort(reason="model_unsupported")
model_long = self._model + " " + self._wifi_version.split("@")[0]
model = self._model
model_family = self._model[:6]

if model in model_to_class:
_LOGGER.info("Model %s supported", model)
self._model = model
elif model_long in model_to_class:
_LOGGER.info("Model %s supported", model_long)
self._model = model_long
elif model_family in model_to_class:
_LOGGER.info("Model family %s supported", model_family)
self._model = model_family
else:
return self.async_abort(reason="model_unsupported")

# use the device ID as unique_id
unique_id = self._device_id
Expand All @@ -163,7 +169,9 @@ async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowRes
_LOGGER.debug("waiting for async_step_confirm")
return await self.async_step_confirm()

async def async_step_confirm(self, user_input: dict[str, Any] = None) -> FlowResult:
async def async_step_confirm(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Confirm the dhcp discovered data."""
_LOGGER.debug("async_step_confirm called with user_input: %s", user_input)

Expand Down Expand Up @@ -192,7 +200,9 @@ async def async_step_confirm(self, user_input: dict[str, Any] = None) -> FlowRes
description_placeholders={"model": self._model, "name": self._name},
)

async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult:
async def async_step_user(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle initial step of user config flow."""

errors = {}
Expand All @@ -202,7 +212,7 @@ async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult
try:
# first some sanitycheck on the host input
if not host_valid(user_input[CONF_HOST]):
raise InvalidHost()
raise InvalidHost # noqa: TRY301
self._host = user_input[CONF_HOST]
_LOGGER.debug("trying to configure host: %s", self._host)

Expand All @@ -225,7 +235,7 @@ async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult
if client is not None:
await client.shutdown()

except asyncio.TimeoutError:
except TimeoutError:
_LOGGER.warning(
r"Timeout, host %s doesn't answer, aborting", self._host
)
Expand Down Expand Up @@ -254,6 +264,9 @@ async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult
self._model = first_model[:9]
_LOGGER.debug("model type extracted: %s", self._model)

# autodetect Wifi version
self._wifi_version = status.get(PhilipsApi.WIFI_VERSION)

# autodetect name
self._name = list(
filter(
Expand Down Expand Up @@ -281,14 +294,21 @@ async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult
)

# check if model is supported
if self._model not in model_to_class:
_LOGGER.info(
"Model %s not supported. Trying model family", self._model
)
self._model = self._model[:6]
if self._model not in model_to_class:
return self.async_abort(reason="model_unsupported")
user_input[CONF_MODEL] = self._model
model_long = self._model + " " + self._wifi_version.split("@")[0]
model = self._model
model_family = self._model[:6]

if model in model_to_class:
_LOGGER.info("Model %s supported", model)
user_input[CONF_MODEL] = model
elif model_long in model_to_class:
_LOGGER.info("Model %s supported", model_long)
user_input[CONF_MODEL] = model_long
elif model_family in model_to_class:
_LOGGER.info("Model family %s supported", model_family)
user_input[CONF_MODEL] = model_family
else:
return self.async_abort(reason="model_unsupported")

# use the device ID as unique_id
unique_id = self._device_id
Expand Down
24 changes: 21 additions & 3 deletions custom_components/philips_airpurifier_coap/const.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Constants for Philips AirPurifier integration."""

from __future__ import annotations

from enum import StrEnum
Expand Down Expand Up @@ -90,14 +91,21 @@ class ICON(StrEnum):

SWITCH_ON = "on"
SWITCH_OFF = "off"
SWITCH_MEDIUM = "medium"
OPTIONS = "options"
DIMMABLE = "dimmable"


class FanModel(StrEnum):
"""Supported fan models."""

AC0850 = "AC0850"
AC0850_11 = "AC0850/11 AWS_Philips_AIR"
AC0850_11C = "AC0850/11 AWS_Philips_AIR_Combo"
AC0850_20 = "AC0850/20 AWS_Philips_AIR"
AC0850_20C = "AC0850/20 AWS_Philips_AIR_Combo"
AC0850_31 = "AC0850/31"
AC0950 = "AC0950"
AC0951 = "AC0951"
AC1214 = "AC1214"
AC1715 = "AC1715"
AC2729 = "AC2729"
Expand All @@ -112,6 +120,7 @@ class FanModel(StrEnum):
AC3055 = "AC3055"
AC3059 = "AC3059"
AC3259 = "AC3259"
AC3421 = "AC3421"
AC3737 = "AC3737"
AC3829 = "AC3829"
AC3836 = "AC3836"
Expand Down Expand Up @@ -153,6 +162,7 @@ class PresetMode:
SLEEP = "sleep"
SLEEP_ALLERGY = "allergy sleep"
TURBO = "turbo"
MEDIUM = "medium"
GAS = "gas"
POLLUTION = "pollution"
LOW = "low"
Expand Down Expand Up @@ -361,6 +371,7 @@ class PhilipsApi:
NEW2_POWER = "D03102"
NEW2_DISPLAY_BACKLIGHT = "D0312D"
NEW2_DISPLAY_BACKLIGHT2 = "D03105"
NEW2_DISPLAY_BACKLIGHT3 = "D03105#1" # dimmable in 3 steps
NEW2_TEMPERATURE = "D03224"
NEW2_SOFTWARE_VERSION = "D01S12"
NEW2_CHILD_LOCK = "D03103"
Expand Down Expand Up @@ -390,7 +401,6 @@ class PhilipsApi:
NEW2_AUTO_PLUS_AI = "D03180"
NEW2_PREFERRED_INDEX = "D0312A#1"
NEW2_GAS_PREFERRED_INDEX = "D0312A#2"
NEW2_ERROR_CODE = "D03240 "

PREFERRED_INDEX_MAP = {
0: ("Indoor Allergen Index", ICON.IAI),
Expand Down Expand Up @@ -601,7 +611,6 @@ class PhilipsApi:
}



EXTRA_SENSOR_TYPES: dict[str, SensorDescription] = {}

BINARY_SENSOR_TYPES: dict[str, SensorDescription] = {
Expand Down Expand Up @@ -795,6 +804,15 @@ class PhilipsApi:
SWITCH_OFF: 0,
DIMMABLE: True,
},
PhilipsApi.NEW2_DISPLAY_BACKLIGHT3: {
ATTR_ICON: ICON.LIGHT_DIMMING_BUTTON,
FanAttributes.LABEL: FanAttributes.DISPLAY_BACKLIGHT,
CONF_ENTITY_CATEGORY: EntityCategory.CONFIG,
SWITCH_ON: 123,
SWITCH_OFF: 0,
SWITCH_MEDIUM: 115,
DIMMABLE: True,
},
}

SELECT_TYPES: dict[str, SelectDescription] = {
Expand Down
Loading

0 comments on commit b1f5695

Please sign in to comment.