Skip to content

Commit

Permalink
Code cleanup. Library version bump.
Browse files Browse the repository at this point in the history
  • Loading branch information
nbogojevic committed Dec 20, 2021
1 parent f27ee5b commit 61410f5
Show file tree
Hide file tree
Showing 11 changed files with 188 additions and 90 deletions.
7 changes: 5 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.linting.flake8Enabled": true,
"python.linting.flake8Args": [
"--ignore=E203,W503",
"--max-line-length=88"
],
"files.associations": {
"*.yaml": "home-assistant"
}
Expand Down
61 changes: 61 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Contribution guidelines

Contributing to this project should be as easy and transparent as possible, whether it's:

- Reporting a bug
- Discussing the current state of the code
- Submitting a fix
- Proposing new features

## Github is used for everything

Github is used to host code, to track issues and feature requests, as well as accept pull requests.

Pull requests are the best way to propose changes to the codebase.

1. Fork the repo and create your branch from `main`.
2. If you've changed something, update the documentation.
3. Make sure your code lints (using [black](https://pypi.org/project/black/) and [flake8](https://pypi.org/project/flake8/)).
4. Test you contribution.
5. Issue that pull request!

## Any contributions you make will be under the MIT Software License

In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.

## Report bugs using Github's [issues](../../issues)

GitHub issues are used to track public bugs.
Report a bug by [opening a new issue](../../issues/new/choose); it's that easy!

## Write bug reports with detail, background, and sample code

**Great Bug Reports** tend to have:

- A quick summary and/or background
- Steps to reproduce
- Be specific!
- Give sample code if you can.
- What you expected would happen
- What actually happens
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)

People *love* thorough bug reports.

## Use a Consistent Coding Style

Use [black](https://github.com/ambv/black) to make sure the code follows the style.

Use [flake8](https://pypi.org/project/flake8/) for linting.

## Test your code modification

This component comes with development environment in a container, easy to launch
if you use Visual Studio Code. With this container you will have a stand alone
Home Assistant instance running and already configured with the included
[`.devcontainer/configuration.yaml`](./.devcontainer/configuration.yaml)
file.

## License

By contributing, you agree that your contributions will be licensed under its MIT License.
23 changes: 20 additions & 3 deletions custom_components/midea_dehumidifier_local/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
The custom component for local network access to Midea Dehumidifier
"""

from __future__ import annotations

from datetime import timedelta
Expand All @@ -22,6 +22,7 @@
CoordinatorEntity,
DataUpdateCoordinator,
)

from midea_beautiful_dehumidifier import appliance_state, connect_to_cloud
from midea_beautiful_dehumidifier.lan import LanDevice
from midea_beautiful_dehumidifier.midea import DEFAULT_APPKEY
Expand Down Expand Up @@ -56,7 +57,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:


class Hub:
"""Central class for interacting with appliances"""

def __init__(self) -> None:
self.coordinators: list[ApplianceUpdateCoordinator] = []

async def start(self, hass: HomeAssistant, data):
"""
sets up appliances and creates an update coordinator for
each appliance
"""
cloud = None
self.coordinators: list[ApplianceUpdateCoordinator] = []
for devconf in data[CONF_DEVICES]:
Expand Down Expand Up @@ -94,21 +104,26 @@ async def start(self, hass: HomeAssistant, data):


class ApplianceUpdateCoordinator(DataUpdateCoordinator):
"""Single class to retrieve data from an appliances"""

def __init__(self, hass, appliance: LanDevice):
super().__init__(
hass,
_LOGGER,
name="Midea appliance",
update_method=self.async_appliance_refresh,
update_method=self._async_appliance_refresh,
update_interval=timedelta(seconds=30),
)
self.appliance = appliance

async def async_appliance_refresh(self):
async def _async_appliance_refresh(self):
"""Called to refresh appliance state"""
await self.hass.async_add_executor_job(self.appliance.refresh)


class ApplianceEntity(CoordinatorEntity):
"""Represents an appliance that gets data from a coordinator"""

def __init__(self, coordinator: ApplianceUpdateCoordinator) -> None:
super().__init__(coordinator)
self.coordinator = coordinator
Expand Down Expand Up @@ -141,10 +156,12 @@ def _updated_data(self):

@property
def name_suffix(self) -> str:
"""Suffix to append to entity name"""
return ""

@property
def unique_id_prefix(self) -> str:
"""Prefix for entity id"""
return ""

@property
Expand Down
24 changes: 12 additions & 12 deletions custom_components/midea_dehumidifier_local/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,39 @@
"""Adds tank full binary sensors for each dehumidifer appliance."""

from custom_components.midea_dehumidifier_local import (
ApplianceUpdateCoordinator,
Hub,
ApplianceEntity,
)
from 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.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from custom_components.midea_dehumidifier_local import ApplianceEntity, Hub
from custom_components.midea_dehumidifier_local.const import DOMAIN


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Sets up full tank binary sensors"""
hub: Hub = hass.data[DOMAIN][config_entry.entry_id]

async_add_entities(
TankFullSensor(coordinator) for coordinator in hub.coordinators
)
async_add_entities(TankFullSensor(coordinator) for coordinator in hub.coordinators)


class TankFullSensor(ApplianceEntity, BinarySensorEntity):
def __init__(self, coordinator: ApplianceUpdateCoordinator) -> None:
super().__init__(coordinator)
"""
Describes full tank binary sensors (indicated as problem as it prevents
dehumidifier from operating)
"""

@property
def name_suffix(self) -> str:
"""Suffix to append to entity name"""
return " Tank Full"

@property
def unique_id_prefix(self) -> str:
"""Prefix for entity id"""
return "midea_dehumidifier_tank_full_"

@property
Expand Down
76 changes: 46 additions & 30 deletions custom_components/midea_dehumidifier_local/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

import ipaddress
import logging
from typing import Tuple
from typing import Any, Tuple

from homeassistant import config_entries, exceptions
from homeassistant import config_entries, data_entry_flow, exceptions
from homeassistant.const import (
CONF_DEVICES,
CONF_ID,
Expand All @@ -16,12 +16,13 @@
CONF_TYPE,
CONF_USERNAME,
)
import voluptuous as vol

from midea_beautiful_dehumidifier import connect_to_cloud, find_appliances
from midea_beautiful_dehumidifier.cloud import MideaCloud
from midea_beautiful_dehumidifier.exceptions import CloudAuthenticationError
from midea_beautiful_dehumidifier.lan import LanDevice, get_appliance_state
from midea_beautiful_dehumidifier.midea import DEFAULT_APPKEY, DISCOVERY_PORT
import voluptuous as vol

from .const import (
CONF_IGNORE_APPLIANCE,
Expand All @@ -35,7 +36,8 @@
_LOGGER = logging.getLogger(__name__)


def validate_input(conf: dict) -> Tuple[MideaCloud, list[LanDevice]]:
def validate_cloud(conf: dict) -> Tuple[MideaCloud, list[LanDevice]]:
"""Validates that cloud credentials are valid and discovers local appliances"""
cloud = connect_to_cloud(
appkey=DEFAULT_APPKEY,
account=conf[CONF_USERNAME],
Expand All @@ -54,13 +56,17 @@ def validate_input(conf: dict) -> Tuple[MideaCloud, list[LanDevice]]:


def validate_appliance(cloud: MideaCloud, appliance: LanDevice):
"""
Validates that appliance configuration is correct and matches physical
device
"""
if appliance.ip == IGNORED_IP_ADDRESS or appliance.ip is None:
_LOGGER.debug("Ignored appliance with id=%s", appliance.id)
return True
try:
ipaddress.IPv4Network(appliance.ip)
except Exception:
raise exceptions.IntegrationError("invalid_ip_address")
except Exception as ex:
raise exceptions.IntegrationError("invalid_ip_address") from ex
discovered = get_appliance_state(ip=appliance.ip, cloud=cloud)
if discovered is not None:
appliance.update(discovered)
Expand All @@ -69,28 +75,39 @@ def validate_appliance(cloud: MideaCloud, appliance: LanDevice):


class MideaLocalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""
Configuration flow for Midea dehumidifiers on local network uses discovery based on
Midea cloud, so it first requires credentials for it.
If some appliances are registered in the cloud, but not discovered, configuration
flow will prompt for additional information.
"""

def __init__(self):
self._cloud_credentials: dict | None = None
self._cloud = None
self._appliance_idx = -1
self._appliances: list[LanDevice] = []
self._appliance_conf = []
self._conf = None

async def async_step_user(self, input: dict):
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> data_entry_flow.FlowResult:
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")

errors: dict = {}

if input is not None:
if user_input is not None:
try:
(
self._cloud,
self._appliances,
) = await self.hass.async_add_executor_job(validate_input, input)
) = await self.hass.async_add_executor_job(validate_cloud, user_input)
self._appliance_idx = -1
self._conf = input
for i, a in enumerate(self._appliances):
if not a.ip:
self._conf = user_input
for i, appliance in enumerate(self._appliances):
if not appliance.ip:
self._appliance_idx = i
break
if self._appliance_idx >= 0:
Expand All @@ -112,21 +129,23 @@ async def async_step_user(self, input: dict):
errors=errors,
)

async def async_step_unreachable_appliance(self, input=None):
async def async_step_unreachable_appliance(self, user_input=None):
"""Manage the appliances that were not found on LAN."""
errors: dict = {}
appliance = self._appliances[self._appliance_idx]

if input is not None:
if user_input is not None:
appliance.ip = (
input[CONF_IP_ADDRESS]
if not input[CONF_IGNORE_APPLIANCE]
user_input[CONF_IP_ADDRESS]
if not user_input[CONF_IGNORE_APPLIANCE]
else IGNORED_IP_ADDRESS
)
appliance.port = DISCOVERY_PORT
appliance.name = input[CONF_NAME]
appliance.token = input[CONF_TOKEN] if CONF_TOKEN in input else ""
appliance.key = input[CONF_TOKEN_KEY] if CONF_TOKEN_KEY in input else ""
appliance.name = user_input[CONF_NAME]
appliance.token = user_input[CONF_TOKEN] if CONF_TOKEN in user_input else ""
appliance.key = (
user_input[CONF_TOKEN_KEY] if CONF_TOKEN_KEY in user_input else ""
)
try:
await self.hass.async_add_executor_job(
validate_appliance,
Expand All @@ -147,9 +166,6 @@ async def async_step_unreachable_appliance(self, input=None):

except exceptions.IntegrationError as ex:
errors["base"] = str(ex)
except Exception:
logging.error("Exception while validating appliance", exc_info=True)
errors["base"] = "invalid_ip_address"

name = appliance.name
return self.async_show_form(
Expand All @@ -173,16 +189,16 @@ async def async_step_unreachable_appliance(self, input=None):
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:
for appliance in self._appliances:
if appliance.ip != IGNORED_IP_ADDRESS:
self._appliance_conf.append(
{
CONF_IP_ADDRESS: a.ip,
CONF_ID: a.id,
CONF_NAME: a.name,
CONF_TYPE: a.type,
CONF_TOKEN: a.token,
CONF_TOKEN_KEY: a.key,
CONF_IP_ADDRESS: appliance.ip,
CONF_ID: appliance.id,
CONF_NAME: appliance.name,
CONF_TYPE: appliance.type,
CONF_TOKEN: appliance.token,
CONF_TOKEN_KEY: appliance.key,
}
)
existing_entry = await self.async_set_unique_id(self._conf[CONF_USERNAME])
Expand Down
Loading

0 comments on commit 61410f5

Please sign in to comment.