-
-
Notifications
You must be signed in to change notification settings - Fork 38.2k
Add HomeKit Controller Diffuser #42676
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 26 commits
0993e1c
905d651
c7c8065
ab06362
cf18a0f
1759d59
32907fb
b4d8f08
d1af0f7
f20e37e
51def50
0ae6c8a
fa2e5bb
8c3ee09
14ede68
a655bb8
bdae947
cbe21aa
8a94e30
fc68adb
ae9e441
da60c21
fc80a29
f1086bc
97db089
ac3d981
ad1649e
3139581
16da871
a2bf653
47de760
f9d2f38
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,11 @@ | ||
| """Support for HomeKit Controller humidifier.""" | ||
| from typing import List, Optional | ||
| from typing import Any, Dict, List, Optional | ||
|
|
||
| from aiohomekit.model.characteristics import CharacteristicsTypes | ||
|
|
||
| from homeassistant.components.humidifier import HumidifierEntity | ||
| from homeassistant.components.humidifier.const import ( | ||
| ATTR_HUMIDITY, | ||
| DEVICE_CLASS_DEHUMIDIFIER, | ||
| DEVICE_CLASS_HUMIDIFIER, | ||
| MODE_AUTO, | ||
|
|
@@ -248,6 +249,92 @@ def unique_id(self) -> str: | |
| return f"homekit-{serial}-{self._iid}-{self.device_class}" | ||
|
|
||
|
|
||
| class HomeKitDiffuser(HomeKitEntity, HumidifierEntity): | ||
| """Representation of a HomeKit Controller Humidifier.""" | ||
|
|
||
| def get_characteristic_types(self): | ||
| """Define the homekit characteristics the entity cares about.""" | ||
| return [ | ||
| CharacteristicsTypes.ACTIVE, | ||
| CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It doesn't look like these are all used, can we trim them out? |
||
| CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE, | ||
| CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE, | ||
| CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL, | ||
| ] | ||
|
|
||
| @property | ||
| def device_class(self) -> str: | ||
| """Return the device class of the device.""" | ||
| return DEVICE_CLASS_HUMIDIFIER | ||
|
|
||
| @property | ||
| def is_on(self): | ||
| """Return true if device is on.""" | ||
| return self.service.value(CharacteristicsTypes.ACTIVE) | ||
|
|
||
| async def async_turn_on(self, **kwargs): | ||
| """Turn the specified valve on.""" | ||
| await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: True}) | ||
|
|
||
| async def async_turn_off(self, **kwargs): | ||
| """Turn the specified valve off.""" | ||
| await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False}) | ||
|
|
||
| @property | ||
| def state_attributes(self) -> Dict[str, Any]: | ||
| """Return the optional state attributes.""" | ||
| data = { | ||
| ATTR_HUMIDITY: self.service.value( | ||
| CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL | ||
| ) | ||
| * 20, | ||
| } | ||
|
|
||
| if not self.is_on: | ||
| data[ATTR_HUMIDITY] = 0 | ||
|
|
||
| return data | ||
|
|
||
| @property | ||
| def available_modes(self) -> Optional[List[str]]: | ||
| """Return a list of available modes. | ||
|
|
||
| Requires SUPPORT_MODES. | ||
| """ | ||
| return [] | ||
|
|
||
| @property | ||
| def target_humidity(self) -> Optional[int]: | ||
| """Return the humidity we try to reach.""" | ||
| if not self.is_on: | ||
| return 0 | ||
|
|
||
| return ( | ||
| self.service.value( | ||
| CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL | ||
| ) | ||
| * 20 | ||
| ) | ||
|
|
||
| async def async_set_humidity(self, humidity: int) -> None: | ||
| """Set new target humidity.""" | ||
|
|
||
| if humidity < 20: | ||
| await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False}) | ||
| else: | ||
| if not self.is_on: | ||
| await self.async_put_characteristics( | ||
| {CharacteristicsTypes.ACTIVE: True} | ||
| ) | ||
|
|
||
| await self.async_put_characteristics( | ||
| { | ||
| CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: humidity | ||
| / 20 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think aiohomekit clamps this to the minStep automatically so this never sends floats right? |
||
| } | ||
| ) | ||
|
|
||
|
|
||
| async def async_setup_entry(hass, config_entry, async_add_entities): | ||
| """Set up Homekit humidifer.""" | ||
| hkid = config_entry.data["AccessoryPairingID"] | ||
|
|
@@ -266,14 +353,19 @@ def get_service(acc, iid): | |
| return None | ||
|
|
||
| def get_char(serv, iid): | ||
| try: | ||
| type_name = CharacteristicsTypes[iid] | ||
| type_uuid = CharacteristicsTypes.get_uuid(type_name) | ||
| for char in serv.get("characteristics"): | ||
| if char.get("type") == type_uuid: | ||
| return char | ||
| except KeyError: | ||
| return None | ||
|
|
||
| if len(iid) == 36: | ||
| type_uuid = iid | ||
| else: | ||
| try: | ||
| type_name = CharacteristicsTypes[iid] | ||
| type_uuid = CharacteristicsTypes.get_uuid(type_name) | ||
| except KeyError: | ||
| return None | ||
|
|
||
| for char in serv.get("characteristics"): | ||
| if char.get("type") == type_uuid: | ||
| return char | ||
| return None | ||
|
|
||
| @callback | ||
|
|
@@ -286,18 +378,26 @@ def async_add_service(aid, service): | |
| serv = get_service(acc, service["iid"]) | ||
|
|
||
| if ( | ||
| get_char(serv, CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD) | ||
| is not None | ||
| ): | ||
| async_add_entities([HomeKitHumidifier(conn, info)], True) | ||
|
|
||
| if ( | ||
| get_char( | ||
| serv, CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD | ||
| ) | ||
| get_char(serv, CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL) | ||
| is not None | ||
| ): | ||
| async_add_entities([HomeKitDehumidifier(conn, info)], True) | ||
| async_add_entities([HomeKitDiffuser(conn, info)], True) | ||
| else: | ||
| if ( | ||
| get_char( | ||
| serv, CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD | ||
| ) | ||
| is not None | ||
| ): | ||
| async_add_entities([HomeKitHumidifier(conn, info)], True) | ||
|
|
||
| if ( | ||
| get_char( | ||
| serv, CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD | ||
| ) | ||
| is not None | ||
| ): | ||
| async_add_entities([HomeKitDehumidifier(conn, info)], True) | ||
|
|
||
| return True | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| """ | ||
| Test against characteristics captured from a VOCOLinc Flowerbud. | ||
|
|
||
| https://github.com/home-assistant/core/issues/26180 | ||
| """ | ||
| from aiohomekit.model.characteristics import CharacteristicsTypes | ||
| from aiohomekit.model.services import ServicesTypes | ||
|
|
||
| from homeassistant.components.humidifier import DOMAIN | ||
|
|
||
| from tests.components.homekit_controller.common import setup_test_component | ||
|
|
||
| ACTIVE = ("humidifier-dehumidifier", "active") | ||
| CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE = ( | ||
| "humidifier-dehumidifier", | ||
| "humidifier-dehumidifier.state.current", | ||
| ) | ||
| TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE = ( | ||
| "humidifier-dehumidifier", | ||
| "humidifier-dehumidifier.state.target", | ||
| ) | ||
| RELATIVE_HUMIDITY_CURRENT = ("humidifier-dehumidifier", "relative-humidity.current") | ||
|
|
||
| VOCOLINC_HUMIDIFIER_SPRAY_LEVEL = ( | ||
| "humidifier-dehumidifier", | ||
| f"Unknown Characteristic {CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL}", | ||
| ) | ||
|
|
||
|
|
||
| def create_diffuser_service(accessory): | ||
|
adrum marked this conversation as resolved.
Outdated
|
||
| """Define a diffuser accessory with VOCOLinc vendor specific characteristics.""" | ||
| service = accessory.add_service(ServicesTypes.HUMIDIFIER_DEHUMIDIFIER) | ||
|
|
||
| service.add_char(CharacteristicsTypes.ACTIVE, value=False) | ||
|
|
||
| cur_state = service.add_char(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT) | ||
| cur_state.value = 0 | ||
|
|
||
| cur_state = service.add_char( | ||
| CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE | ||
| ) | ||
| cur_state.value = -1 | ||
|
|
||
| targ_state = service.add_char( | ||
| CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE | ||
| ) | ||
| targ_state.value = 0 | ||
|
|
||
| targ_state = service.add_char( | ||
| CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL | ||
| ) | ||
| targ_state.value = 0 | ||
|
|
||
| return service | ||
|
|
||
|
|
||
| async def test_diffuser_read_state(hass, utcnow): | ||
| """Test that we can read the state of a HomeKit diffuser accessory.""" | ||
| helper = await setup_test_component(hass, create_diffuser_service) | ||
|
|
||
| helper.characteristics[VOCOLINC_HUMIDIFIER_SPRAY_LEVEL].value = 5 | ||
| state = await helper.poll_and_get_state() | ||
| assert state.attributes["humidity"] == 0 | ||
|
|
||
| helper.characteristics[ACTIVE].value = 1 | ||
| state = await helper.poll_and_get_state() | ||
| assert state.attributes["humidity"] == 100 | ||
|
|
||
| helper.characteristics[VOCOLINC_HUMIDIFIER_SPRAY_LEVEL].value = 2 | ||
| state = await helper.poll_and_get_state() | ||
| assert state.attributes["humidity"] == 40 | ||
|
|
||
| helper.characteristics[ACTIVE].value = 0 | ||
| state = await helper.poll_and_get_state() | ||
| assert state.attributes["humidity"] == 0 | ||
|
|
||
|
|
||
| async def test_diffuser_set_state(hass, utcnow): | ||
| """Test that we can set the state of a HomeKit diffuser accessory.""" | ||
| helper = await setup_test_component(hass, create_diffuser_service) | ||
|
|
||
| await hass.services.async_call( | ||
| DOMAIN, | ||
| "set_humidity", | ||
| {"entity_id": helper.entity_id, "humidity": 20}, | ||
| blocking=True, | ||
| ) | ||
| assert helper.characteristics[VOCOLINC_HUMIDIFIER_SPRAY_LEVEL].value == 1 | ||
|
|
||
| await hass.services.async_call( | ||
| DOMAIN, | ||
| "set_humidity", | ||
| {"entity_id": helper.entity_id, "humidity": 60}, | ||
| blocking=True, | ||
| ) | ||
| assert helper.characteristics[VOCOLINC_HUMIDIFIER_SPRAY_LEVEL].value == 3 | ||
|
|
||
| await hass.services.async_call( | ||
| DOMAIN, | ||
| "turn_off", | ||
| {"entity_id": helper.entity_id}, | ||
| blocking=True, | ||
| ) | ||
| assert helper.characteristics[ACTIVE].value == 0 | ||
|
|
||
| await hass.services.async_call( | ||
| DOMAIN, | ||
| "turn_on", | ||
| {"entity_id": helper.entity_id}, | ||
| blocking=True, | ||
| ) | ||
| assert helper.characteristics[ACTIVE].value == 1 | ||
|
|
||
| await hass.services.async_call( | ||
| DOMAIN, | ||
| "set_humidity", | ||
| {"entity_id": helper.entity_id, "humidity": 19}, | ||
| blocking=True, | ||
| ) | ||
| assert helper.characteristics[ACTIVE].value == 0 | ||
Uh oh!
There was an error while loading. Please reload this page.