Skip to content
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

Switch heating modes #16

Merged
merged 6 commits into from
May 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions .github/workflows/lint.yml → .github/workflows/lint_and_test.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: "Lint"
name: "Lint and Test"

on:
push:
Expand All @@ -9,8 +9,8 @@ on:
- "main"

jobs:
ruff:
name: "Ruff"
lint_and_test:
name: Lint and run tests
runs-on: "ubuntu-latest"
steps:
- name: "Checkout the repository"
Expand All @@ -25,5 +25,11 @@ jobs:
- name: "Install requirements"
run: python3 -m pip install -r requirements.txt

- name: "Run"
run: python3 -m ruff check .
- name: Lint Check
run: ruff check custom_components

- name: Format check
run: ruff format --check custom_components

- name: Run tests
run: python -m pytest -v test
9 changes: 6 additions & 3 deletions .ruff.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml

line-length = 120
target-version = "py310"
exclude = ["examples"]

[lint]
select = [
"B007", # Loop control variable {name} not used within loop body
"B014", # Exception handler with duplicate exception
Expand Down Expand Up @@ -39,11 +42,11 @@ ignore = [
"E731", # do not assign a lambda expression, use a def
]

[flake8-pytest-style]
[lint.flake8-pytest-style]
fixture-parentheses = false

[pyupgrade]
[lint.pyupgrade]
keep-runtime-typing = true

[mccabe]
[lint.mccabe]
max-complexity = 25
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
ha-ecodan
=========

A [Home Assistant](https://www.home-assistant.io/)
integration for the [Mitsubishi Ecodan](https://les.mitsubishielectric.co.uk/products/residential-heating/outdoor)
Heatpumps


pyecodan
--------

A client for interacting with the MELCloud service for controlling the heatpump.
The intention is for this to be replaced by a local controller as detailed by
@rbroker in [ecodan-ha-local](https://github.com/rbroker/ecodan-ha-local)


Development
-----------

Development is based on [HACS](https://hacs.xyz/docs/categories/integrations/)

### Testing

A Dockerfile is provided for testing in an isolated Home Assistant instance.

```
docker build . -t hass
docker run --rm -it -p 8123:8123 -v ${PWD}:/hass /bin/bash

$ source /opt/hass/core/venv/bin activate
$ ./scripts/develop
```

Then open a web browser at `http://localhost:8123`


See Also
--------

Home Assistant Core includes a mature integration using MELCloud with support for
heat pumps and air conditioners, however the underlying Python library (`pymelcloud`)
is no longer under active development.
10 changes: 2 additions & 8 deletions custom_components/ha_ecodan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@
from .coordinator import EcodanDataUpdateCoordinator
from .pyecodan import Client

PLATFORMS: list[Platform] = [
Platform.SWITCH,
Platform.SENSOR
]
PLATFORMS: list[Platform] = [Platform.SWITCH, Platform.SENSOR, Platform.SELECT]


# https://developers.home-assistant.io/docs/config_entries_index/#setting-up-an-entry
Expand All @@ -27,10 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
session=async_get_clientsession(hass),
)
device = await client.get_device(entry.data[CONF_DEVICE_ID])
hass.data[DOMAIN][entry.entry_id] = coordinator = EcodanDataUpdateCoordinator(
hass=hass,
device=device
)
hass.data[DOMAIN][entry.entry_id] = coordinator = EcodanDataUpdateCoordinator(hass=hass, device=device)
# https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities
await coordinator.async_config_entry_first_refresh()

Expand Down
37 changes: 9 additions & 28 deletions custom_components/ha_ecodan/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Adds config flow for Blueprint."""

from __future__ import annotations

import voluptuous as vol
Expand All @@ -19,8 +20,8 @@ class EcodanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1

async def async_step_user(
self,
user_input: dict | None = None,
self,
user_input: dict | None = None,
) -> config_entries.FlowResult:
"""Handle a flow initialized by the user."""
_errors = {}
Expand Down Expand Up @@ -50,24 +51,17 @@ async def async_step_user(
CONF_USERNAME,
default=(user_input or {}).get(CONF_USERNAME),
): TextSelector(
TextSelectorConfig(
type=TextSelectorType.TEXT
),
TextSelectorConfig(type=TextSelectorType.TEXT),
),
vol.Required(CONF_PASSWORD): TextSelector(
TextSelectorConfig(
type=TextSelectorType.PASSWORD
),
TextSelectorConfig(type=TextSelectorType.PASSWORD),
),
}
),
errors=_errors,
)

async def async_step_select_device(
self,
user_input: dict
) -> config_entries.FlowResult:
async def async_step_select_device(self, user_input: dict) -> config_entries.FlowResult:
"""List the available devices to the user."""

_errors = {}
Expand All @@ -77,24 +71,11 @@ async def async_step_select_device(
await self.async_set_unique_id(device_id)
self._abort_if_unique_id_configured()
data = dict(self._account)
data.update({
"device_id": device_id,
"device_name": device_name
})
return self.async_create_entry(
title=data[CONF_USERNAME], data=data
)
data.update({"device_id": device_id, "device_name": device_name})
return self.async_create_entry(title=data[CONF_USERNAME], data=data)
else:
return self.async_show_form(
step_id="select_device",
data_schema=vol.Schema(
{
"device": selector({
"select": {
"options": list(self._devices.keys())
}
})
}
),
data_schema=vol.Schema({"device": selector({"select": {"options": list(self._devices.keys())}})}),
errors=_errors,
)
5 changes: 3 additions & 2 deletions custom_components/ha_ecodan/const.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""Constants for integration_blueprint."""

from logging import Logger, getLogger

LOGGER: Logger = getLogger(__package__)

NAME = "Ecodan"
DOMAIN = "ha_ecodan"
VERSION = "0.1.0"
ATTRIBUTION = "Data provided by http://jsonplaceholder.typicode.com/"
VERSION = "0.1.1"
ATTRIBUTION = "https://github.com/planetmarshall/ha-ecodan.git"
CONF_DEVICE_ID = "device_id"
3 changes: 2 additions & 1 deletion custom_components/ha_ecodan/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ class EcodanEntity(CoordinatorEntity):
"""Base class for Ecodan entities."""

_attr_attribution = ATTRIBUTION
_attr_has_entity_name = True

def __init__(self, coordinator: EcodanDataUpdateCoordinator) -> None:
"""Initialize."""
super().__init__(coordinator)
self._attr_unique_id = coordinator.config_entry.entry_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self.unique_id)},
identifiers={(DOMAIN, coordinator.device.id)},
name=NAME,
model=VERSION,
manufacturer=NAME,
Expand Down
2 changes: 1 addition & 1 deletion custom_components/ha_ecodan/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"documentation": "https://github.com/planetmarshall/ha-ecodan",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/planetmarshall/ha-ecodan/issues",
"version": "0.1.0"
"version": "0.1.1"
}
22 changes: 11 additions & 11 deletions custom_components/ha_ecodan/pyecodan/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ class Client:

base_url = "https://app.melcloud.com/Mitsubishi.Wifi.Client"

def __init__(self,
username: str = os.getenv("ECODAN_USERNAME"),
password: str = os.getenv("ECODAN_PASSWORD"),
session: ClientSession = None):
"""
Create a client with the supplied credentials.
def __init__(
self,
username: str = os.getenv("ECODAN_USERNAME"),
password: str = os.getenv("ECODAN_PASSWORD"),
session: ClientSession = None,
):
"""Create a client with the supplied credentials.

:param username: MELCloud username. Default is taken from the environment variable `ECODAN_USERNAME`
:param password: MELCloud password. Default is taken from the environment variable `ECODAN_PASSWORD`
Expand Down Expand Up @@ -54,7 +55,7 @@ async def login(self) -> None:
"Language": 0,
"AppVersion": "1.26.2.0",
"Persist": True,
"CaptchaResponse": None
"CaptchaResponse": None,
}
async with self._session.post(login_url, json=login_data) as response:
response_data = await response.json()
Expand All @@ -79,14 +80,12 @@ async def list_devices(self) -> dict:
structure = location["Structure"]
location_name = location["Name"]
for device in structure["Devices"]:
devices[device["DeviceName"]] = {
"location_name": location_name,
"id": device["DeviceID"]
}
devices[device["DeviceName"]] = {"location_name": location_name, "id": device["DeviceID"]}

return devices

async def __aenter__(self) -> "Client":
"""Enter context manager."""
return self

async def __aexit__(
Expand All @@ -95,4 +94,5 @@ async def __aexit__(
exc_val,
exc_tb,
) -> None:
"""Exit context manager."""
await self._session.__aexit__(exc_type, exc_val, exc_tb)
Loading
Loading