diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6e4761 --- /dev/null +++ b/.gitignore @@ -0,0 +1,129 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4f63669 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Nenad Bogojevic + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..443a75a --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +This is a custom component for Home assistant that adds support for Midea dehumidifier appliances via the local area network. + +# midea-dehumidifier-lan +Home Assistant custom component for controlling Midea dehumidiferes on local network + +## Installation instruction + +### HACS +Add this repository as custom integration repository to HACS. + +### Manual +1. Update Home Assistant to version 2021.12 or newer +2. Clone this repository +3. Copy the `custom_components/midea_dehumidifier_local` folder into your Home Assistant's `custom_components` folder + +### Configuring +1. Add `Midea Dehumidifer (LAN)` integration via UI +2. Enter Midea cloud username and password. Those are the same used in Midea mobile application. +3. The integration will discover dehumidifiers on local LAN network(s). +4. If a dehumidifer is not automatically discovered, but is registered to the cloud account, user is prompted to enter IPv4 address of the dehumidifier. + +## Known issues + +* If IPv4 address of dehumidifer changes, new IPv4 address will not be used until Home Assistant's restart + + +## Supported entities + +This custom component creates following entites for each discovered dehumidifer: + +* humidifier/dehumidifer +* fan +* sensor with current humidity +* binary sensor for full tank +* switch for controlling ION mode (switch has no effect if dehumidifier doesn't support it) + +## See also + +https://github.com/nbogojevic/midea-beautiful-dehumidifier diff --git a/custom_components/midea_dehumidifier_local/__init__.py b/custom_components/midea_dehumidifier_local/__init__.py new file mode 100644 index 0000000..6d09321 --- /dev/null +++ b/custom_components/midea_dehumidifier_local/__init__.py @@ -0,0 +1,60 @@ +""" +The custom component for local network access to Midea Dehumidifier + +""" +from __future__ import annotations + +import logging + +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DEVICES, CONF_IP_ADDRESS, CONF_USERNAME, CONF_PASSWORD, CONF_TOKEN +from .const import ( + DOMAIN, + PLATFORMS, + CONF_APP_KEY, + CONF_TOKEN_KEY +) + +from midea_beautiful_dehumidifier import appliance_state, connect_to_cloud + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry +) -> bool: + """Set up platform from a ConfigEntry.""" + hass.data.setdefault(DOMAIN, {}) + _LOGGER.debug("Configuration entry: %s", entry.data) + + hub = await hass.async_add_executor_job(Hub, hass, entry.data) + hass.data[DOMAIN][entry.entry_id] = hub + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + # This is called when an entry/configured device is to be removed. The class + # needs to unload itself, and remove callbacks. See the classes for further + # details + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + +class Hub: + def __init__(self, hass: HomeAssistant, data): + self.cloud = connect_to_cloud( + appkey=data[CONF_APP_KEY], + account=data[CONF_USERNAME], + password=data[CONF_PASSWORD], + ) + self.appliances = [] + for aconf in data[CONF_DEVICES]: + app = appliance_state(aconf[CONF_IP_ADDRESS], token=aconf[CONF_TOKEN], key=aconf[CONF_TOKEN_KEY], cloud=self.cloud) + self.appliances.append(app) + + \ No newline at end of file diff --git a/custom_components/midea_dehumidifier_local/binary_sensor.py b/custom_components/midea_dehumidifier_local/binary_sensor.py new file mode 100644 index 0000000..74b2ac9 --- /dev/null +++ b/custom_components/midea_dehumidifier_local/binary_sensor.py @@ -0,0 +1,51 @@ +from midea_beautiful_dehumidifier.lan import LanDevice +from config.custom_components.midea_dehumidifier_local.const import DOMAIN +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity_platform import AddEntitiesCallback + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + hub = hass.data[DOMAIN][config_entry.entry_id] + + # Add all entities to HA + async_add_entities( + TankFullSensor(appliance) for appliance in hub.appliances + ) + + +class TankFullSensor(BinarySensorEntity): + def __init__(self, appliance: LanDevice) -> None: + super().__init__() + self._appliance = appliance + self._unique_id = f"midea_dehumidifier_tank_full_{appliance.id}" + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + + @property + def name(self): + """Return the unique id.""" + return str(getattr(self._appliance.state, "name", self.unique_id)) + " Tank Full" + + @property + def is_on(self): + return getattr(self._appliance.state, "tank_full", False) + + @property + def device_info(self): + return { + "identifiers": { + (DOMAIN, self._appliance.sn) + }, + "name": self.name, + "manufacturer": "Midea", + "model": str(self._appliance.type), + } \ No newline at end of file diff --git a/custom_components/midea_dehumidifier_local/config_flow.py b/custom_components/midea_dehumidifier_local/config_flow.py new file mode 100644 index 0000000..5b4e485 --- /dev/null +++ b/custom_components/midea_dehumidifier_local/config_flow.py @@ -0,0 +1,238 @@ +"""Config flow for Midea Dehumidifier (Local) integration.""" +from __future__ import annotations + +import ipaddress +import logging +from typing import Tuple + +from midea_beautiful_dehumidifier import find_appliances, connect_to_cloud +from midea_beautiful_dehumidifier.cloud import MideaCloud +from midea_beautiful_dehumidifier.lan import LanDevice, get_appliance_state +import voluptuous as vol + +from homeassistant import config_entries, exceptions +from homeassistant.const import ( + CONF_DEVICES, + CONF_ID, + CONF_IP_ADDRESS, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_TOKEN, + CONF_TYPE, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv + +from .const import ( + DOMAIN, + DEFAULT_ANNOUNCE_PORT, + CONF_APP_KEY, + CONF_TOKEN_KEY, + CONF_IGNORE_APPLIANCE, + IGNORED_IP_ADDRESS, +) + +_LOGGER = logging.getLogger(__name__) + + +def validate_input( + hass: HomeAssistant, conf: dict +) -> Tuple[MideaCloud, list[LanDevice]]: + cloud = connect_to_cloud( + appkey=conf[CONF_APP_KEY], + account=conf[CONF_USERNAME], + password=conf[CONF_PASSWORD], + ) + if cloud is None: + raise exceptions.IntegrationError("no_cloud") + appliances = find_appliances( + cloud=cloud, + broadcast_retries=2, + broadcast_timeout=3, + ) + for appliance in appliances: + _LOGGER.info("%s", appliance) + return cloud, appliances + + +def validate_appliance( + hass: HomeAssistant, cloud: MideaCloud, appliance: LanDevice +): + _LOGGER.debug("Validating id=%s ip=%s", appliance.id, appliance.ip) + _LOGGER.debug(" token=%s key=%s", appliance.token, appliance.key) + if appliance.ip == IGNORED_IP_ADDRESS: + _LOGGER.debug("Ignored appliance with id=%s", appliance.ip) + return True + ipaddress.IPv4Network(appliance.ip) + discovered = get_appliance_state(ip=appliance.ip, cloud=cloud) + if discovered is not None: + appliance.update(discovered) + return True + raise exceptions.IntegrationError("not_discovered") + + + +class MideaLocalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + def __init__(self): + self._cloud_credentials: dict | None = None + self._cloud = None + self._appliance_idx = -1 + self._appliances: list[LanDevice] = [] + + async def async_step_user(self, user_input: dict): + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + errors: dict = {} + + if user_input is not None: + ( + self._cloud, + self._appliances, + ) = await self.hass.async_add_executor_job( + validate_input, self.hass, user_input + ) + self._appliance_idx = -1 + self._conf = user_input + for i, a in enumerate(self._appliances): + if not a.ip: + self._appliance_idx = i + break + if self._appliance_idx >= 0: + return await self.async_step_unreachable_appliance() + + return await self._async_add_entry() + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_USERNAME, default="" + ): vol.All(cv.string, vol.Length(min=3)), + vol.Required(CONF_PASSWORD, default=""): vol.All( + cv.string, vol.Length(min=6) + ), + vol.Required( + CONF_APP_KEY, + default="3742e9e5842d4ad59c2db887e12449f9", + ): str, + } + ), + errors=errors, + ) + + async def async_step_unreachable_appliance(self, user_input=None): + """Manage the appliances that were not found on LAN.""" + errors: dict = {} + if user_input is not None: + _LOGGER.info( + "async_step_unreachable_appliance(user_input)=%s", user_input + ) + self._appliances[self._appliance_idx].ip = ( + user_input[CONF_IP_ADDRESS] + if not user_input[CONF_IGNORE_APPLIANCE] + else IGNORED_IP_ADDRESS + ) + self._appliances[self._appliance_idx].port = user_input[CONF_PORT] + self._appliances[self._appliance_idx].token = ( + user_input[CONF_TOKEN] + if hasattr(user_input, CONF_TOKEN) + else "" + ) + self._appliances[self._appliance_idx].key = ( + user_input[CONF_TOKEN_KEY] + if hasattr(user_input, CONF_TOKEN_KEY) + else "" + ) + try: + await self.hass.async_add_executor_job( + validate_appliance, + self.hass, + self._cloud, + self._appliances[self._appliance_idx], + ) + # Find next unreachable appliance + self._appliance_idx = self._appliance_idx + 1 + while self._appliance_idx < len(self._appliances): + if self._appliances[self._appliance_idx].ip is None: + return await self.async_step_unreachable_appliance() + self._appliance_idx = self._appliance_idx + 1 + + # If no unreachable appliances, create entry + if self._appliance_idx >= len(self._appliances): + return await self._async_add_entry() + except Exception: + logging.error("Exception while validating appliance", exc_info=True) + errors["base"] = "invalid_ip_address" + + _LOGGER.info( + "Showing unreachable appliance! %d %s", + self._appliance_idx, + self._appliances[self._appliance_idx].id, + ) + return self.async_show_form( + step_id="unreachable_appliance", + data_schema=vol.Schema( + { + vol.Required(CONF_IGNORE_APPLIANCE, default=False): bool, + vol.Optional( + CONF_IP_ADDRESS, default=IGNORED_IP_ADDRESS + ): str, + vol.Required( + CONF_PORT, default=DEFAULT_ANNOUNCE_PORT + ): cv.port, + vol.Optional(CONF_TOKEN): str, + vol.Optional(CONF_TOKEN_KEY): str, + } + ), + description_placeholders={ + CONF_ID: self._appliances[self._appliance_idx].id + if self._appliance_idx < len(self._appliances) + else "", + CONF_NAME: self._appliances[self._appliance_idx].state.name + if self._appliance_idx < len(self._appliances) + else "", + }, + errors=errors, + ) + + async def _async_add_entry(self): + if self._conf is not None: + self._appliance_conf = [] + for a in self._appliances: + if a.ip != IGNORED_IP_ADDRESS: + self._appliance_conf.append({ + CONF_IP_ADDRESS: a.ip, + CONF_ID: a.id, + CONF_NAME: a.state.name, + CONF_TYPE: a.type, + CONF_TOKEN: a.token, + CONF_TOKEN_KEY: a.key, + }) + existing_entry = await self.async_set_unique_id( + self._conf[CONF_USERNAME] + ) + self._conf[CONF_DEVICES] = self._appliance_conf + if existing_entry: + self.hass.config_entries.async_update_entry( + existing_entry, + data=self._conf, + ) + # Reload the config entry otherwise devices will remain unavailable + self.hass.async_create_task( + self.hass.config_entries.async_reload( + existing_entry.entry_id + ) + ) + + return self.async_abort(reason="reauth_successful") + else: + return self.async_create_entry( + title="Midea Dehumidifiers", + data=self._conf, + ) + else: + raise exceptions.InvalidStateError("unexpected_state") diff --git a/custom_components/midea_dehumidifier_local/const.py b/custom_components/midea_dehumidifier_local/const.py new file mode 100644 index 0000000..b83c51f --- /dev/null +++ b/custom_components/midea_dehumidifier_local/const.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from typing import Final +from homeassistant.const import Platform + +__version__ = "0.0.1" + +# Base component constants +NAME = "Midea Dehumidifier (LAN)" +DOMAIN = "midea_dehumidifier_local" +DOMAIN_DATA = f"{DOMAIN}_data" +ISSUE_URL = "https://github.com/nbogojevic/midea_dehumidifier_local/issues" + +CONF_APP_KEY = "app_key" +CONF_TOKEN_KEY = "token_key" +CONF_IGNORE_APPLIANCE = "ignore_appliance" + +PLATFORMS: Final = [Platform.FAN, Platform.HUMIDIFIER, Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] + +IGNORED_IP_ADDRESS = "0.0.0.0" +DEFAULT_ANNOUNCE_PORT = 6445 + +STARTUP_MESSAGE = f""" +------------------------------------------------------------------- +{NAME} +Version: {__version__} +This is a custom integration! +If you have any issues with this you need to open an issue here: +{ISSUE_URL} +------------------------------------------------------------------- +""" \ No newline at end of file diff --git a/custom_components/midea_dehumidifier_local/fan.py b/custom_components/midea_dehumidifier_local/fan.py new file mode 100644 index 0000000..1f7da01 --- /dev/null +++ b/custom_components/midea_dehumidifier_local/fan.py @@ -0,0 +1,60 @@ +from midea_beautiful_dehumidifier.lan import LanDevice +from config.custom_components.midea_dehumidifier_local.const import DOMAIN +from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity_platform import AddEntitiesCallback + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + hub = hass.data[DOMAIN][config_entry.entry_id] + + # Add all entities to HA + async_add_entities( + DehumidiferFan(appliance) for appliance in hub.appliances + ) + + +class DehumidiferFan(FanEntity): + def __init__(self, appliance: LanDevice) -> None: + super().__init__() + self._appliance = appliance + self._unique_id = f"midea_dehumidifier_fan_{appliance.id}" + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + + @property + def name(self): + """Return the unique id.""" + return str(getattr(self._appliance.state, "name", self.unique_id)) + " Fan" + + @property + def percentage(self): + return getattr(self._appliance.state, "fan_speed", 0) + + @property + def supported_features(self): + return SUPPORT_SET_SPEED + + def set_percentage(self, percentage: int) -> None: + """Set the speed percentage of the fan.""" + setattr(self._appliance.state, "fan_speed", percentage) + self._appliance.apply() + + @property + def device_info(self): + return { + "identifiers": { + (DOMAIN, self._appliance.sn) + }, + "name": str(getattr(self._appliance.state, "name", self.unique_id)), + "manufacturer": "Midea", + "model": str(self._appliance.type), + } \ No newline at end of file diff --git a/custom_components/midea_dehumidifier_local/humidifier.py b/custom_components/midea_dehumidifier_local/humidifier.py new file mode 100644 index 0000000..be3ae5a --- /dev/null +++ b/custom_components/midea_dehumidifier_local/humidifier.py @@ -0,0 +1,160 @@ +import logging + +from midea_beautiful_dehumidifier.lan import LanDevice + +from config.custom_components.midea_dehumidifier_local.const import DOMAIN +from homeassistant.components.humidifier import ( + HumidifierDeviceClass, + HumidifierEntity, +) +from homeassistant.components.humidifier.const import ( + MODE_AUTO, + MODE_BOOST, + MODE_COMFORT, + MODE_NORMAL, + SUPPORT_MODES, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +_LOGGER = logging.getLogger(__name__) +AVAILABLE_MODES = [MODE_AUTO, MODE_NORMAL, MODE_BOOST, MODE_COMFORT] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + + hub = hass.data[DOMAIN][config_entry.entry_id] + + # Add all entities to HA + async_add_entities( + DehumidifierEntity(appliance) for appliance in hub.appliances + ) + + +class DehumidifierEntity(HumidifierEntity): + def __init__(self, appliance: LanDevice) -> None: + super().__init__() + self._appliance = appliance + self._unique_id = f"midea_dehumidifier_{appliance.id}" + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + + @property + def name(self): + """Return the unique id.""" + return str(getattr(self._appliance.state, "name", self.unique_id)) + + @property + def should_poll(self): + """Return the polling state.""" + # get device's status by polling it + return True + + @property + def is_on(self): + return getattr(self._appliance.state, "is_on", False) + + @property + def device_class(self): + return HumidifierDeviceClass.DEHUMIDIFIER + + @property + def target_humidity(self): + return getattr(self._appliance.state, "target_humidity", 0) + + @property + def supported_features(self): + return SUPPORT_MODES + + @property + def available_modes(self): + return AVAILABLE_MODES + + @property + def mode(self): + curr_mode = getattr(self._appliance.state, "mode", 1) + if curr_mode == 1: + return MODE_NORMAL + if curr_mode == 2: + return MODE_COMFORT + if curr_mode == 3: + return MODE_AUTO + if curr_mode == 4: + return MODE_BOOST + _LOGGER.warn("Unknown mode %d", curr_mode) + return MODE_NORMAL + + @property + def min_humidity(self): + """Return the min humidity set.""" + return 40 + + @property + def max_humidity(self): + """Return the max humidity set.""" + return 85 + + @property + def fan_speed(self): + return getattr(self._appliance.state, "fan_speed", 0) + + def turn_on(self, **kwargs): + """Turn the entity on.""" + setattr(self._appliance.state, "is_on", True) + self._appliance.apply() + + def turn_off(self, **kwargs): + """Turn the entity off.""" + setattr(self._appliance.state, "is_on", False) + self._appliance.apply() + + def set_mode(self, mode): + """Set new target preset mode.""" + if mode == MODE_NORMAL: + curr_mode = 1 + elif mode == MODE_COMFORT: + curr_mode = 2 + elif mode == MODE_AUTO: + curr_mode = 3 + elif mode == MODE_BOOST: + curr_mode = 4 + else: + _LOGGER.warn("Unsupported dehumidifer mode %s", mode) + curr_mode = 1 + setattr(self._appliance.state, "mode", curr_mode) + self._appliance.apply() + + def set_humidity(self, humidity): + """Set new target humidity.""" + setattr(self._appliance.state, "target_humidity", False) + self._appliance.apply() + + def update(self) -> None: + self._appliance.refresh() + + @property + def extra_state_attributes(self): + """Return entity specific state attributes.""" + + return { + "fan_speed": self.fan_speed, + } + + @property + def device_info(self): + return { + "identifiers": { + (DOMAIN, self._appliance.sn) + }, + "name": str(getattr(self._appliance.state, "name", self.unique_id)), + "manufacturer": "Midea", + "model": str(self._appliance.type), + } \ No newline at end of file diff --git a/custom_components/midea_dehumidifier_local/manifest.json b/custom_components/midea_dehumidifier_local/manifest.json new file mode 100644 index 0000000..c75642c --- /dev/null +++ b/custom_components/midea_dehumidifier_local/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "midea_dehumidifier_local", + "name": "Midea Dehumidifier (LAN)", + "version": "0.0.1", + "config_flow": true, + "documentation": "https://github.com/nbogojevic/midea-dehumidifier-lan/main/README.md", + "issue_tracker": "https://github.com/nbogojevic/midea-dehumidifier-lan/issues", + "codeowners": ["@nbogojevic"], + "requirements": ["midea-beautiful-dehumidifier==0.0.10"], + "iot_class": "local_polling", + "render_readme": true, + "homeassistant": "2021.12.0" +} \ No newline at end of file diff --git a/custom_components/midea_dehumidifier_local/sensor.py b/custom_components/midea_dehumidifier_local/sensor.py new file mode 100644 index 0000000..a215cc8 --- /dev/null +++ b/custom_components/midea_dehumidifier_local/sensor.py @@ -0,0 +1,63 @@ +from midea_beautiful_dehumidifier.lan import LanDevice +from config.custom_components.midea_dehumidifier_local.const import DOMAIN +from homeassistant.components.sensor import SensorEntity +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity_platform import AddEntitiesCallback + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + hub = hass.data[DOMAIN][config_entry.entry_id] + + # Add all entities to HA + async_add_entities( + CurrentHumiditySensor(appliance) for appliance in hub.appliances + ) + + +class CurrentHumiditySensor(SensorEntity): + def __init__(self, appliance: LanDevice) -> None: + super().__init__() + self._appliance = appliance + self._unique_id = f"midea_dehumidifier_humidity_{appliance.id}" + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + + @property + def name(self): + """Return the unique id.""" + return str(getattr(self._appliance.state, "name", self.unique_id)) + " Humidity" + + @property + def device_class(self): + return "humidity" + + @property + def native_value(self): + return getattr(self._appliance.state, "current_humidity", None) + + @property + def native_unit_of_measurement(self): + return "%" + + @property + def state_class(self): + return "measurement" + + @property + def device_info(self): + return { + "identifiers": { + (DOMAIN, self._appliance.sn) + }, + "name": str(getattr(self._appliance.state, "name", self.unique_id)), + "manufacturer": "Midea", + "model": str(self._appliance.type), + } \ No newline at end of file diff --git a/custom_components/midea_dehumidifier_local/switch.py b/custom_components/midea_dehumidifier_local/switch.py new file mode 100644 index 0000000..7cdbb9d --- /dev/null +++ b/custom_components/midea_dehumidifier_local/switch.py @@ -0,0 +1,67 @@ +from midea_beautiful_dehumidifier.lan import LanDevice +from config.custom_components.midea_dehumidifier_local.const import DOMAIN +from homeassistant.components.switch import SwitchEntity +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity_platform import AddEntitiesCallback + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + + hub = hass.data[DOMAIN][config_entry.entry_id] + + # Add all entities to HA + async_add_entities(IonSwitch(appliance) for appliance in hub.appliances) + + +class IonSwitch(SwitchEntity): + def __init__(self, appliance: LanDevice) -> None: + super().__init__() + self._appliance = appliance + self._unique_id = f"midea_dehumidifier_ion_mode_{appliance.id}" + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + + @property + def name(self): + """Return the unique id.""" + return str(getattr(self._appliance.state, "name", self.unique_id)) + " Ion Mode" + + @property + def icon(self): + return "mdi:air-purifier" + + @property + def is_on(self): + return getattr(self._appliance.state, "ion_mode", False) + + def turn_on(self, **kwargs): + """Turn the entity on.""" + setattr(self._appliance.state, "ion_mode", True) + self._appliance.apply() + + def turn_off(self, **kwargs): + """Turn the entity off.""" + setattr(self._appliance.state, "ion_mode", False) + self._appliance.apply() + + def update(self) -> None: + self._appliance.refresh() + + @property + def device_info(self): + return { + "identifiers": { + (DOMAIN, self._appliance.sn) + }, + "name": str(getattr(self._appliance.state, "name", self.unique_id)), + "manufacturer": "Midea", + "model": str(self._appliance.type), + } \ No newline at end of file diff --git a/custom_components/midea_dehumidifier_local/translations/en.json b/custom_components/midea_dehumidifier_local/translations/en.json new file mode 100644 index 0000000..54e1dd1 --- /dev/null +++ b/custom_components/midea_dehumidifier_local/translations/en.json @@ -0,0 +1,41 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Username", + "password": "Password", + "app_key": "App key" + }, + "description": "Please enter Midea cloud credentials. Those are the ones used from the mobile application.", + "title": "Midea cloud credentials" + }, + "unreachable_appliance": { + "data": { + "id": "Appliance ID", + "ip_address": "IP address", + "port": "IP discovery port", + "token": "Token", + "token_key": "Token key", + "ignore_appliance": "Ignore this appliance" + }, + "description": "We were unable to discover an appliance with id {id}, name {name} on the local network. If you know its address, please enter it to add it to Home Assistant.", + "title": "Appliance address missing" + } + }, + "error": { + "api_problem": "Bad response from server", + "cannot_connect": "Failed to connect to server", + "invalid_auth": "Invalid credentials", + "unsupported": "Unsupported operation", + "unknown": "An unknown error occurred", + "invalid_ip_address": "Invalid IPv4 address, please enter valid IPv4 address.", + "not_discovered": "Unable to find appliance at specified IPv4 address." + }, + "abort": { + "already_configured": "Already configured", + "single_instance_allowed": "Already defined a Midea cloud account. Only a single account is supported for dehumidifier integration." + } + } + } + \ No newline at end of file diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..bdf20a9 --- /dev/null +++ b/hacs.json @@ -0,0 +1,14 @@ +{ + "domain": "midea_dehumidifier_local", + "name": "Midea Dehumidifier (LAN)", + "version": "0.0.1", + "config_flow": true, + "documentation": "https://github.com/nbogojevic/midea-dehumidifier-lan/main/README.md", + "issue_tracker": "https://github.com/nbogojevic/midea-dehumidifier-lan/issues", + "codeowners": ["@nbogojevic"], + "requirements": ["midea-beautiful-dehumidifier==0.0.10"], + "iot_class": "local_polling", + "render_readme": true, + "homeassistant": "2021.12.0" + } + \ No newline at end of file