├── .gitignore ├── CODEOWNERS ├── README.md ├── custom_components └── coway │ ├── __init__.py │ ├── air_quality.py │ ├── config_flow.py │ ├── const.py │ ├── fan.py │ ├── manifest.json │ ├── services.yaml │ ├── strings.json │ ├── switch.py │ └── translations │ └── en.json └── info.md /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | README.md @sarahenkens 2 | CODEOWNERS @sarahhenkens 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Coway IoCare 2 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs) ![LGTM Grade](https://img.shields.io/lgtm/grade/python/github/sarahhenkens/home-assistant-iocare) ![GitHub manifest version (path)](https://img.shields.io/github/manifest-json/v/sarahhenkens/home-assistant-iocare?filename=custom_components%2Fcoway%2Fmanifest.json) ![GitHub all releases](https://img.shields.io/github/downloads/sarahhenkens/home-assistant-iocare/total?color=green) 3 | 4 | Custom component for Home Assistant Core for monitoring and controlling 5 | Coway / Airmega air purifiers. 6 | 7 | **Breaking Change 2021.4.0** 8 | 9 | The recent update of home-assistant-iocare to `2021.4.0` uses a new domain. If you previously installed any version prior to `2021.4.0`, you will need to delete the integration from the integrations page in Home Assistant. Due to the new domain name, previous integrations will fail to work. 10 | 1. Delete your current IOCare Integration from the Home Assistant Integrations page 11 | 1. If you updated prior to deleting the integration, delete the integration, restart Home Assistant, and proceed to step 4 12 | 2. Update home-assistant-iocare via HACS or manually 13 | 3. Restart Home Assistant 14 | 4. Initiate Config Flow by navigating to Configuration > Integrations > click the "+" button > find "Coway IoCare" 15 | 5. Enter your Coway IoCare credentials 16 | 17 | ## Installation 18 | 19 | ### With HACS 20 | 1. Open HACS Settings and add this repository (https://github.com/sarahhenkens/home-assistant-iocare) 21 | as a Custom Repository (use **Integration** as the category). 22 | 2. The `Coway IoCare` page should automatically load (or find it in the HACS Store) 23 | 3. Click `Install` 24 | 25 | ### Manual 26 | Copy the `coway` directory from `custom_components` in this repository, 27 | and place inside your Home Assistant Core installation's `custom_components` directory. 28 | 29 | 30 | ## Setup 31 | 1. Install this integration. 32 | 2. Use Config Flow to configure the integration with your Coway IoCare credentials. 33 | * Initiate Config Flow by navigating to Configuration > Integrations > click the "+" button > find "Coway IoCare" (restart Home Assistant and / or clear browser cache if you can't find it) 34 | -------------------------------------------------------------------------------- /custom_components/coway/__init__.py: -------------------------------------------------------------------------------- 1 | """Support for Coway IoCare""" 2 | import asyncio 3 | import logging 4 | import voluptuous as vol 5 | from iocare import IOCareApi 6 | 7 | import homeassistant.helpers.config_validation as cv 8 | from homeassistant import config_entries 9 | from homeassistant.const import EVENT_HOMEASSISTANT_STOP 10 | from homeassistant.exceptions import ConfigEntryNotReady 11 | from homeassistant.const import ( 12 | CONF_PASSWORD, 13 | CONF_USERNAME 14 | ) 15 | from .const import DOMAIN 16 | 17 | 18 | _LOGGER = logging.getLogger(__name__) 19 | 20 | 21 | def setup(hass, config): 22 | """Setup of the component""" 23 | return True 24 | 25 | 26 | async def async_setup_entry(hass, config_entry): 27 | """Set up IoCare integration from a config entry.""" 28 | username = config_entry.data.get(CONF_USERNAME) 29 | password = config_entry.data.get(CONF_PASSWORD) 30 | 31 | _LOGGER.info("Initializing the IOCare API") 32 | iocare = await hass.async_add_executor_job(IOCareApi, username, password) 33 | _LOGGER.info("Connected to API") 34 | 35 | hass.data[DOMAIN] = iocare 36 | 37 | hass.async_add_job( 38 | hass.config_entries.async_forward_entry_setup(config_entry, "fan") 39 | ) 40 | hass.async_add_job( 41 | hass.config_entries.async_forward_entry_setup(config_entry, "air_quality") 42 | ) 43 | hass.async_add_job( 44 | hass.config_entries.async_forward_entry_setup(config_entry, "switch") 45 | ) 46 | 47 | return True 48 | -------------------------------------------------------------------------------- /custom_components/coway/air_quality.py: -------------------------------------------------------------------------------- 1 | """Support for Coway Air Quality Sensor.""" 2 | 3 | import logging 4 | from homeassistant.components.air_quality import AirQualityEntity 5 | from .const import DOMAIN 6 | 7 | 8 | _LOGGER = logging.getLogger(__name__) 9 | 10 | 11 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 12 | """Platform uses config entry setup.""" 13 | pass 14 | 15 | 16 | async def async_setup_entry(hass, config_entry, async_add_entities): 17 | """Set up Coway Air Quality device.""" 18 | _LOGGER.info("Setting up config entry for the Air Quality platform") 19 | 20 | iocare = hass.data[DOMAIN] 21 | 22 | devices = [] 23 | for device in iocare.devices(): 24 | devices.append(AirMonitor(device)) 25 | 26 | async_add_entities(devices) 27 | 28 | 29 | class AirMonitor(AirQualityEntity): 30 | """Representation of a Coway Airmega air monitor.""" 31 | 32 | def __init__(self, device): 33 | self._device = device 34 | self._available = True 35 | 36 | @property 37 | def unique_id(self): 38 | """Return the ID of this purifier.""" 39 | return self._device.device_id 40 | 41 | @property 42 | def name(self): 43 | """Return the name of the purifier if any.""" 44 | return self._device.name 45 | 46 | @property 47 | def air_quality_index(self): 48 | """Return the Air Quality Index (AQI).""" 49 | return round(float(self._device.quality["air_quality_index"]), 1) 50 | 51 | @property 52 | def particulate_matter_2_5(self): 53 | """Return the particulate matter 2.5 level.""" 54 | return self._device.quality["particulate_matter_2_5"] 55 | 56 | @property 57 | def particulate_matter_10(self): 58 | """Return the particulate matter 10 level.""" 59 | return self._device.quality["particulate_matter_10"] 60 | 61 | @property 62 | def carbon_dioxide(self): 63 | """Return the CO2 (carbon dioxide) level.""" 64 | return self._device.quality["carbon_dioxide"] 65 | 66 | @property 67 | def volatile_organic_compounds(self): 68 | """Return the VOC (Volatile Organic Compounds) level.""" 69 | return self._device.quality["volatile_organic_compounds"] 70 | 71 | @property 72 | def state(self): 73 | """Return the current state.""" 74 | return self.air_quality_index 75 | 76 | def update(self): 77 | """Update automation state.""" 78 | _LOGGER.info("Refreshing device state") 79 | self._device.refresh() 80 | -------------------------------------------------------------------------------- /custom_components/coway/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow of our component""" 2 | import logging 3 | import voluptuous as vol 4 | from iocare.iocareapi import IOCareApi 5 | from homeassistant import config_entries 6 | from homeassistant.core import callback 7 | from homeassistant.const import ( 8 | CONF_PASSWORD, 9 | CONF_USERNAME 10 | ) 11 | from .const import DOMAIN 12 | _LOGGER = logging.getLogger(__name__) 13 | 14 | class IoCareConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 15 | """Handle our config flow.""" 16 | 17 | VERSION = 1 18 | CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL 19 | 20 | def __init__(self): 21 | """Initialize IoCare configuration flow""" 22 | self.schema = vol.Schema({ 23 | vol.Required(CONF_USERNAME): str, 24 | vol.Required(CONF_PASSWORD): str 25 | }) 26 | 27 | self._username = None 28 | self._password = None 29 | 30 | async def async_step_user(self, user_input=None): 31 | """Handle a flow start.""" 32 | 33 | if self._async_current_entries(): 34 | return self.async_abort(reason="already_configured") 35 | 36 | if not user_input: 37 | return self._show_form() 38 | 39 | self._username = user_input[CONF_USERNAME] 40 | self._password = user_input[CONF_PASSWORD] 41 | 42 | return await self._async_iocare_login() 43 | 44 | 45 | async def _async_iocare_login(self): 46 | 47 | errors = {} 48 | 49 | try: 50 | client = await self.hass.async_add_executor_job(IOCareApi, self._username, self._password) 51 | await self.hass.async_add_executor_job(client.check_access_token) 52 | 53 | except Exception: 54 | _LOGGER.error("Unable to connect to IoCare: Failed to Log In") 55 | errors = {"base": "auth_error"} 56 | 57 | if errors: 58 | return self._show_form(errors=errors) 59 | 60 | return await self._async_create_entry() 61 | 62 | async def _async_create_entry(self): 63 | """Create the config entry.""" 64 | config_data = { 65 | CONF_USERNAME: self._username, 66 | CONF_PASSWORD: self._password, 67 | } 68 | 69 | return self.async_create_entry(title=self._username, data=config_data) 70 | 71 | @callback 72 | def _show_form(self, errors=None): 73 | """Show the form to the user.""" 74 | return self.async_show_form( 75 | step_id="user", 76 | data_schema=self.schema, 77 | errors=errors if errors else {}, 78 | ) 79 | -------------------------------------------------------------------------------- /custom_components/coway/const.py: -------------------------------------------------------------------------------- 1 | """My platform constants""" 2 | 3 | DOMAIN = "coway" 4 | 5 | IOCARE_FAN_OFF = "0" 6 | IOCARE_FAN_LOW = "1" 7 | IOCARE_FAN_MEDIUM = "2" 8 | IOCARE_FAN_HIGH = "3" 9 | -------------------------------------------------------------------------------- /custom_components/coway/fan.py: -------------------------------------------------------------------------------- 1 | """Support for Coway Air Purifiers.""" 2 | 3 | import logging 4 | import voluptuous as vol 5 | from homeassistant.helpers import config_validation as cv, entity_platform 6 | from homeassistant.const import ATTR_ENTITY_ID 7 | from homeassistant.util.percentage import ordered_list_item_to_percentage 8 | from homeassistant.components.fan import ( 9 | FanEntity, 10 | SUPPORT_SET_SPEED, 11 | SUPPORT_PRESET_MODE, 12 | ) 13 | 14 | 15 | """Attributes""" 16 | 17 | ATTR_NIGHT_MODE = "night_mode" 18 | ATTR_AUTO_MODE = "auto_mode" 19 | ATTR_PRE_FILTER_PERCENT = "pre_filter_percent" 20 | ATTR_MAX2_FILTER_PERCENT = "max2_filter_percent" 21 | 22 | SERVICE_SET_AUTO_MODE = "set_auto_mode_on" 23 | SERVICE_SET_NIGHT_MODE = "set_night_mode_on" 24 | 25 | PRESET_MODE_AUTO = "auto" 26 | PRESET_MODE_NIGHT = "night" 27 | 28 | 29 | from .const import ( 30 | DOMAIN, 31 | IOCARE_FAN_OFF, 32 | IOCARE_FAN_LOW, 33 | IOCARE_FAN_MEDIUM, 34 | IOCARE_FAN_HIGH 35 | ) 36 | 37 | ORDERED_NAMED_FAN_SPEEDS = [IOCARE_FAN_LOW, IOCARE_FAN_MEDIUM, IOCARE_FAN_HIGH] 38 | PRESET_MODES = [PRESET_MODE_AUTO, PRESET_MODE_NIGHT] 39 | 40 | 41 | IOCARE_FAN_SPEED_TO_HASS = { 42 | IOCARE_FAN_OFF: 0, 43 | IOCARE_FAN_LOW: ordered_list_item_to_percentage(ORDERED_NAMED_FAN_SPEEDS, IOCARE_FAN_LOW), 44 | IOCARE_FAN_MEDIUM: ordered_list_item_to_percentage(ORDERED_NAMED_FAN_SPEEDS, IOCARE_FAN_MEDIUM), 45 | IOCARE_FAN_HIGH: ordered_list_item_to_percentage(ORDERED_NAMED_FAN_SPEEDS, IOCARE_FAN_HIGH) 46 | } 47 | 48 | HASS_FAN_SPEED_TO_IOCARE = {v: k for (k, v) in IOCARE_FAN_SPEED_TO_HASS.items()} 49 | 50 | 51 | _LOGGER = logging.getLogger(__name__) 52 | 53 | 54 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 55 | """Platform uses config entry setup.""" 56 | pass 57 | 58 | 59 | async def async_setup_entry(hass, config_entry, async_add_entities): 60 | """Set up Coway Air Purifier devices.""" 61 | iocare = hass.data[DOMAIN] 62 | 63 | platform = entity_platform.current_platform.get() 64 | 65 | devices = [] 66 | 67 | for device in iocare.devices(): 68 | devices.append(AirPurifier(device)) 69 | 70 | async_add_entities(devices) 71 | 72 | platform.async_register_entity_service( 73 | SERVICE_SET_AUTO_MODE, 74 | {vol.Required(ATTR_ENTITY_ID): cv.entity_id}, 75 | "set_auto_mode_on", 76 | ) 77 | 78 | platform.async_register_entity_service( 79 | SERVICE_SET_NIGHT_MODE, 80 | {vol.Required(ATTR_ENTITY_ID): cv.entity_id}, 81 | "set_night_mode_on", 82 | ) 83 | 84 | class AirPurifier(FanEntity): 85 | """Representation of a Coway Airmega air purifier.""" 86 | 87 | def __init__(self, device): 88 | self._device = device 89 | self._available = True 90 | 91 | @property 92 | def unique_id(self): 93 | """Return the ID of this purifier.""" 94 | return self._device.device_id 95 | 96 | @property 97 | def name(self): 98 | """Return the name of the purifier if any.""" 99 | return self._device.name 100 | 101 | @property 102 | def is_on(self): 103 | """Return true if the purifier is on""" 104 | return self._device.is_on 105 | 106 | @property 107 | def preset_modes(self): 108 | """Return the available preset modes""" 109 | return PRESET_MODES 110 | 111 | @property 112 | def preset_mode(self): 113 | """"Return the current preset mode""" 114 | if self._device.is_auto_eco: 115 | return PRESET_MODE_AUTO 116 | if self._device.is_auto: 117 | return PRESET_MODE_AUTO 118 | if self._device.is_night: 119 | return PRESET_MODE_NIGHT 120 | return None 121 | 122 | @property 123 | def auto_mode(self): 124 | """Return true if purifier speed is set to auto mode and false if off.""" 125 | if self._device.is_auto_eco: 126 | return str(self._device.is_auto_eco).lower() + "(eco)" 127 | return self._device.is_auto 128 | 129 | @property 130 | def night_mode(self): 131 | """Return true if purifier speed is set to night mode and false if off.""" 132 | return self._device.is_night 133 | 134 | @property 135 | def pre_filter_percent(self) -> int: 136 | """Return Pre-Filter Percentage""" 137 | return self._device.filters[0]["life_level_pct"] 138 | 139 | @property 140 | def max2_filter_percent(self) -> int: 141 | """Return MAX2 Filter Percentage""" 142 | return self._device.filters[1]["life_level_pct"] 143 | 144 | @property 145 | def available(self): 146 | """Return true if switch is available.""" 147 | return self._available 148 | 149 | @property 150 | def percentage(self) -> int: 151 | """Return the current speed.""" 152 | if not self._device.is_on: 153 | return 0 154 | return IOCARE_FAN_SPEED_TO_HASS.get(self._device.fan_speed) 155 | 156 | @property 157 | def speed_count(self) -> int: 158 | """Get the list of available speeds.""" 159 | return len(ORDERED_NAMED_FAN_SPEEDS) 160 | 161 | @property 162 | def supported_features(self) -> int: 163 | """Flag supported features.""" 164 | return SUPPORT_SET_SPEED | SUPPORT_PRESET_MODE 165 | 166 | @property 167 | def device_state_attributes(self) -> dict: 168 | """Return optional state attributes.""" 169 | return { 170 | ATTR_NIGHT_MODE: self.night_mode, 171 | ATTR_AUTO_MODE: self.auto_mode, 172 | ATTR_PRE_FILTER_PERCENT: self.pre_filter_percent, 173 | ATTR_MAX2_FILTER_PERCENT: self.max2_filter_percent, 174 | } 175 | 176 | def turn_on(self, percentage: int = None, **kwargs) -> None: 177 | """Turn the air purifier on.""" 178 | self._device.set_power(True) 179 | if percentage is not None: 180 | self.set_percentage(percentage) 181 | 182 | def turn_off(self, **kwargs) -> None: 183 | """Turn the air purifier off.""" 184 | self._device.set_power(False) 185 | 186 | def set_percentage(self, percentage: int) -> None: 187 | """Set the fan_mode of the air purifier.""" 188 | if percentage == 0: 189 | return self.turn_off() 190 | if not self.is_on: 191 | self.turn_on() 192 | self._device.set_fan_speed(HASS_FAN_SPEED_TO_IOCARE.get(percentage)) 193 | self._device.set_fan_speed(HASS_FAN_SPEED_TO_IOCARE.get(percentage)) 194 | 195 | def set_preset_mode(self, preset_mode: str) -> None: 196 | """Set a preset mode on the fan.""" 197 | if preset_mode == PRESET_MODE_AUTO: 198 | if not self._device.is_on: 199 | self.turn_on() 200 | self._device.set_auto_mode() 201 | self._device.set_auto_mode() 202 | if preset_mode == PRESET_MODE_NIGHT: 203 | if not self._device.is_on: 204 | self.turn_on() 205 | self._device.set_night_mode() 206 | self._device.set_night_mode() 207 | 208 | def set_auto_mode_on(self) -> None: 209 | """Sets Auto Mode to ON""" 210 | self._device.set_auto_mode() 211 | 212 | def set_night_mode_on(self) -> None: 213 | """Sets Night Mode to ON""" 214 | self._device.set_night_mode() 215 | 216 | def update(self): 217 | """Update automation state.""" 218 | _LOGGER.info("Refreshing device state") 219 | self._device.refresh() 220 | -------------------------------------------------------------------------------- /custom_components/coway/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "coway", 3 | "name": "Coway IoCare", 4 | "version": "2021.4.0", 5 | "config_flow": true, 6 | "requirements": ["git+https://github.com/RobertD502/python-iocare@master#python-iocare==0.1.6"], 7 | "dependencies": [], 8 | "codeowners": ["@sarahhenkens"] 9 | } 10 | -------------------------------------------------------------------------------- /custom_components/coway/services.yaml: -------------------------------------------------------------------------------- 1 | set_night_mode_on: 2 | name: Turn On Night Mode 3 | description: Set the purifier to night mode. 4 | fields: 5 | entity_id: 6 | description: Name of the entity to set to night mode 7 | example: "fan.office_purifier" 8 | selector: 9 | entity: 10 | integration: coway 11 | domain: fan 12 | 13 | set_auto_mode_on: 14 | name: Turn On Auto Mode 15 | description: Set the purifier to auto mode. 16 | fields: 17 | entity_id: 18 | description: Name of the entity to set to auto mode 19 | example: "fan.office_purifier" 20 | selector: 21 | entity: 22 | integration: coway 23 | domain: fan 24 | -------------------------------------------------------------------------------- /custom_components/coway/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "title": "Coway IoCare", 4 | "step": { 5 | "user": { 6 | "title": "Fill in your Coway IoCare credentials", 7 | "data": { 8 | "username": "IoCare Username", 9 | "password": "IoCare Password" 10 | } 11 | } 12 | }, 13 | "error": { 14 | "auth_error": "IoCare authentication failed. Are your credentials correct?" 15 | }, 16 | "abort": { 17 | "already_configured": "IoCare is already configured" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /custom_components/coway/switch.py: -------------------------------------------------------------------------------- 1 | """Support for IOCare switches.""" 2 | import logging 3 | from homeassistant.components.switch import SwitchEntity 4 | from .const import DOMAIN 5 | _LOGGER = logging.getLogger(__name__) 6 | 7 | 8 | 9 | def setup_platform(hass, config, add_entities, discovery_info=None): 10 | """Platform uses config entry setup.""" 11 | pass 12 | 13 | async def async_setup_entry(hass, config_entry, async_add_entities): 14 | """Set up Coway Air Purifier devices.""" 15 | iocare = hass.data[DOMAIN] 16 | 17 | devices = [] 18 | 19 | for device in iocare.devices(): 20 | devices.append(IOCareSwitch(device)) 21 | 22 | async_add_entities(devices) 23 | 24 | 25 | class IOCareSwitch(SwitchEntity): 26 | """Representation of a Coway Airmega air purifier switch.""" 27 | 28 | def __init__(self, device): 29 | self._device = device 30 | self._available = True 31 | 32 | @property 33 | def unique_id(self): 34 | """Return the ID of this purifier.""" 35 | return self._device.device_id 36 | 37 | @property 38 | def name(self): 39 | """Return the name of the purifier + Light if any.""" 40 | return self._device.name + " Light" 41 | 42 | @property 43 | def icon(self): 44 | """Set purifier switch icon to lightbulb""" 45 | return 'mdi:lightbulb' 46 | 47 | @property 48 | def is_on(self): 49 | """Return true if switch is on.""" 50 | return self._device.is_light_on 51 | 52 | def turn_on(self, **kwargs): 53 | """Turn the switch on.""" 54 | self._device.set_light(True) 55 | 56 | def turn_off(self, **kwargs): 57 | """Turn the device off.""" 58 | self._device.set_light(False) 59 | -------------------------------------------------------------------------------- /custom_components/coway/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "IoCare is already configured" 5 | }, 6 | "error": { 7 | "auth_error": "IoCare authentication failed. Are your credentials correct?" 8 | }, 9 | "step": { 10 | "user": { 11 | "data": { 12 | "username": "IoCare Username", 13 | "password": "IoCare Password" 14 | }, 15 | "title": "Fill in your Coway IoCare credentials" 16 | } 17 | }, 18 | "title": "Coway IoCare" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /info.md: -------------------------------------------------------------------------------- 1 | ## Setup 2 | 1. Install this integration. 3 | 2. Use Config Flow to configure the integration for your IoCare credentials. --------------------------------------------------------------------------------