From a21df4529c0abd305baa27231561fa0a8b1ae0b2 Mon Sep 17 00:00:00 2001 From: Jonni Liljamo Date: Mon, 4 Nov 2024 15:16:43 +0200 Subject: [PATCH] initial --- .envrc | 1 + .gitignore | 18 +++ .hass_dev/automations.yaml | 1 + .hass_dev/configuration.yaml | 16 +++ .hass_dev/scenes.yaml | 0 .hass_dev/scripts.yaml | 0 custom_components/ouman_eh800/__init__.py | 24 ++++ custom_components/ouman_eh800/config_flow.py | 44 ++++++ custom_components/ouman_eh800/const.py | 15 +++ custom_components/ouman_eh800/eh800.py | 44 ++++++ custom_components/ouman_eh800/manifest.json | 6 + custom_components/ouman_eh800/sensor.py | 87 ++++++++++++ custom_components/ouman_eh800/strings.json | 15 +++ .../ouman_eh800/translations/en.json | 1 + flake.lock | 125 ++++++++++++++++++ flake.nix | 71 ++++++++++ hacs.json | 3 + justfile | 8 ++ requirements.dev.txt | 1 + 19 files changed, 480 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 .hass_dev/automations.yaml create mode 100644 .hass_dev/configuration.yaml create mode 100644 .hass_dev/scenes.yaml create mode 100644 .hass_dev/scripts.yaml create mode 100644 custom_components/ouman_eh800/__init__.py create mode 100644 custom_components/ouman_eh800/config_flow.py create mode 100644 custom_components/ouman_eh800/const.py create mode 100644 custom_components/ouman_eh800/eh800.py create mode 100644 custom_components/ouman_eh800/manifest.json create mode 100644 custom_components/ouman_eh800/sensor.py create mode 100644 custom_components/ouman_eh800/strings.json create mode 120000 custom_components/ouman_eh800/translations/en.json create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 hacs.json create mode 100644 justfile create mode 100644 requirements.dev.txt diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..c4b17d7 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use_flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b811ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +/.direnv/ +/.venv/ +/.pre-commit-config.yaml + +*.pyc + +.hass_dev/* +!.hass_dev/configuration.yaml +!.hass_dev/lovelace.yaml +!.hass_dev/lovelace-mushroom-showcase.yaml +!.hass_dev/lovelace.yaml +!.hass_dev/automations.yaml +!.hass_dev/scripts.yaml +!.hass_dev/scenes.yaml +!.hass_dev/views +!.hass_dev/www +!.hass_dev/packages +.hass_dev/www/community/* diff --git a/.hass_dev/automations.yaml b/.hass_dev/automations.yaml new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/.hass_dev/automations.yaml @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/.hass_dev/configuration.yaml b/.hass_dev/configuration.yaml new file mode 100644 index 0000000..8a00001 --- /dev/null +++ b/.hass_dev/configuration.yaml @@ -0,0 +1,16 @@ + +# Loads default set of integrations. Do not remove. +default_config: + +# Load frontend themes from the themes folder +frontend: + themes: !include_dir_merge_named themes + +automation: !include automations.yaml +script: !include scripts.yaml +scene: !include scenes.yaml + +logger: + default: error + logs: + custom_components.ouman_eh800: debug diff --git a/.hass_dev/scenes.yaml b/.hass_dev/scenes.yaml new file mode 100644 index 0000000..e69de29 diff --git a/.hass_dev/scripts.yaml b/.hass_dev/scripts.yaml new file mode 100644 index 0000000..e69de29 diff --git a/custom_components/ouman_eh800/__init__.py b/custom_components/ouman_eh800/__init__.py new file mode 100644 index 0000000..f43ca29 --- /dev/null +++ b/custom_components/ouman_eh800/__init__.py @@ -0,0 +1,24 @@ +""" +ouman_eh800 +""" +import logging + +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["sensor"] + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + _LOGGER.debug("Setting up Ouman EH-800") + + hass.data.setdefault(DOMAIN, {}) + hass_data = dict(entry.data) + hass.data[DOMAIN][entry.entry_id] = hass_data + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True diff --git a/custom_components/ouman_eh800/config_flow.py b/custom_components/ouman_eh800/config_flow.py new file mode 100644 index 0000000..844cc1d --- /dev/null +++ b/custom_components/ouman_eh800/config_flow.py @@ -0,0 +1,44 @@ +import logging +import voluptuous as vol + +from typing import Any, Dict, Optional + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from .const import DOMAIN, DEFAULT_PORT, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD + +_LOGGER = logging.getLogger(__name__) + +USER_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + +class OumanEH800ConfigFlow(ConfigFlow, domain=DOMAIN): + """Ouman EH-800 config flow""" + + VERSION = 1 + + async def _create_entry(self, host: str, port: int, username: str, password: str) -> ConfigFlowResult: + """Register new entry.""" + return self.async_create_entry( + title=f"Ouman {host}", + data={ + CONF_HOST: host, + CONF_PORT: port, + CONF_USERNAME: username, + CONF_PASSWORD: password + } + ) + + async def async_step_user(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=USER_SCHEMA + ) + + _LOGGER.debug(user_input) + return await self._create_entry(user_input[CONF_HOST], user_input[CONF_PORT], user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) diff --git a/custom_components/ouman_eh800/const.py b/custom_components/ouman_eh800/const.py new file mode 100644 index 0000000..192f429 --- /dev/null +++ b/custom_components/ouman_eh800/const.py @@ -0,0 +1,15 @@ +DOMAIN = "ouman_eh800" + +DEFAULT_PORT = 80 + +CONF_HOST = "host" +CONF_PORT = "port" +CONF_USERNAME = "username" +CONF_PASSWORD = "password" + +TEMPERATURE_SENSOR_TYPE_OUTSIDE = "outside" +TEMPERATURE_SENSOR_TYPE_L1_ROOM = "l1_room" +TEMPERATURE_SENSOR_TYPE_L1_SUPPLY = "l1_supply" +#TEMPERATURE_SENSOR_TYPE_ = "" +#TEMPERATURE_SENSOR_TYPE_ = "" +#TEMPERATURE_SENSOR_TYPE_ = "" diff --git a/custom_components/ouman_eh800/eh800.py b/custom_components/ouman_eh800/eh800.py new file mode 100644 index 0000000..d3bf240 --- /dev/null +++ b/custom_components/ouman_eh800/eh800.py @@ -0,0 +1,44 @@ +import logging +import requests + +_LOGGER = logging.getLogger(__name__) + +class EH800: + def __init__(self, host: str, port: int, username: str, password: str) -> None: + self._uri = f"http://{host}:{port}" + self._login = f"uid={username};pwd={password};" + + def _refresh_login(self) -> bool: + r = requests.get(f"{self._uri}/login?{self._login}") + + if r.text[:-1] == "login?result=ok;": + _LOGGER.debug("Login ok") + return True + else: + _LOGGER.debug("Login error") + return False + + def _request_value(self, register) -> str: + if not self._refresh_login(): + return + + r = requests.get(f"{self._uri}/request?{register}") + eq_index = r.text.find("=") + sc_index = r.text.find(";") + return r.text[eq_index+1:sc_index] + + def get_outside_temp(self) -> float: + return self._outside_temp + def update_outside_temp(self): + self._outside_temp = self._request_value("S_227_85") + + def get_l1_room_temp(self) -> float: + return self._l1_room_temp + def update_l1_room_temp(self): + self._l1_room_temp = self._request_value("S_261_85") + + def get_l1_supply_temp(self) -> float: + return self._l1_supply_temp + def update_l1_supply_temp(self): + self._l1_supply_temp = self._request_value("S_259_85") + diff --git a/custom_components/ouman_eh800/manifest.json b/custom_components/ouman_eh800/manifest.json new file mode 100644 index 0000000..a267b59 --- /dev/null +++ b/custom_components/ouman_eh800/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "ouman_eh800", + "name": "Ouman EH-800", + "config_flow": true, + "version": "0.1.0" +} diff --git a/custom_components/ouman_eh800/sensor.py b/custom_components/ouman_eh800/sensor.py new file mode 100644 index 0000000..94c3ad7 --- /dev/null +++ b/custom_components/ouman_eh800/sensor.py @@ -0,0 +1,87 @@ +import logging + +from datetime import timedelta + +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity, SensorStateClass +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfTemperature +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import Throttle + +from. const import * +from .eh800 import EH800 + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + config = hass.data[DOMAIN][entry.entry_id] + + eh800 = EH800(config[CONF_HOST], config[CONF_PORT], config[CONF_USERNAME], config[CONF_PASSWORD]) + + entities = [] + entities.append(TemperatureSensor(hass, eh800, TEMPERATURE_SENSOR_TYPE_OUTSIDE)) + entities.append(TemperatureSensor(hass, eh800, TEMPERATURE_SENSOR_TYPE_L1_ROOM)) + entities.append(TemperatureSensor(hass, eh800, TEMPERATURE_SENSOR_TYPE_L1_SUPPLY)) + + async_add_entities(entities) + + return True + +class TemperatureSensor(SensorEntity): + def __init__(self, hass: HomeAssistant, api: EH800, sensor_type: str): + self._hass = hass + self._api = api + self._sensor_type = sensor_type + + self._unique_id = f"{DOMAIN}_temperature_{sensor_type}".lower() + + self._state = 0.0 + + @property + def device_class(self): + return SensorDeviceClass.TEMPERATURE + + @property + def device_info(self): + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + } + + @property + def name(self): + return self.unique_id + + @property + def native_unit_of_measurement(self): + return UnitOfTemperature.CELSIUS + + @property + def state(self): + return self._state + + @property + def state_class(self): + return SensorStateClass.MEASUREMENT + + @property + def unique_id(self): + return self._unique_id + + @Throttle(timedelta(minutes=1)) + async def async_update(self): + if self._sensor_type == TEMPERATURE_SENSOR_TYPE_OUTSIDE: + await self._hass.async_add_executor_job(self._api.update_outside_temp) + self._state = self._api.get_outside_temp() + elif self._sensor_type == TEMPERATURE_SENSOR_TYPE_L1_ROOM: + await self._hass.async_add_executor_job(self._api.update_l1_room_temp) + self._state = self._api.get_l1_room_temp() + elif self._sensor_type == TEMPERATURE_SENSOR_TYPE_L1_SUPPLY: + await self._hass.async_add_executor_job(self._api.update_l1_supply_temp) + self._state = self._api.get_l1_supply_temp() diff --git a/custom_components/ouman_eh800/strings.json b/custom_components/ouman_eh800/strings.json new file mode 100644 index 0000000..1e205c4 --- /dev/null +++ b/custom_components/ouman_eh800/strings.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "user": { + "title": "Setup Ouman EH-800", + "data": { + "host": "The hostname or IP address of your Ouman EH-800.", + "port": "The port of your Ouman EH-800 web interface.", + "username": "The username of your Ouman EH-800 account.", + "password": "The password of your Ouman EH-800 account." + } + } + } + } +} diff --git a/custom_components/ouman_eh800/translations/en.json b/custom_components/ouman_eh800/translations/en.json new file mode 120000 index 0000000..ebc8e39 --- /dev/null +++ b/custom_components/ouman_eh800/translations/en.json @@ -0,0 +1 @@ +../strings.json \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..5bcecbe --- /dev/null +++ b/flake.lock @@ -0,0 +1,125 @@ +{ + "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1730504689, + "narHash": "sha256-hgmguH29K2fvs9szpq2r3pz2/8cJd2LPS+b4tfNFCwE=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "506278e768c2a08bec68eb62932193e341f55c90", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "pre-commit-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1730272153, + "narHash": "sha256-B5WRZYsRlJgwVHIV6DvidFN7VX7Fg9uuwkRW9Ha8z+w=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "2d2a9ddbe3f2c00747398f3dc9b05f7f2ebb0f53", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1720386169, + "narHash": "sha256-NGKVY4PjzwAa4upkGtAMz1npHGoRzWotlSnVlqI40mo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "194846768975b7ad2c4988bdb82572c00222c0d7", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "pre-commit-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ], + "nixpkgs-stable": "nixpkgs-stable" + }, + "locked": { + "lastModified": 1730302582, + "narHash": "sha256-W1MIJpADXQCgosJZT8qBYLRuZls2KSiKdpnTVdKBuvU=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "af8a16fe5c264f5e9e18bcee2859b40a656876cf", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": "pre-commit-hooks" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..2c65787 --- /dev/null +++ b/flake.nix @@ -0,0 +1,71 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + flake-parts = { + url = "github:hercules-ci/flake-parts"; + inputs.nixpkgs-lib.follows = "nixpkgs"; + }; + + pre-commit-hooks = { + url = "github:cachix/git-hooks.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = inputs @ { + self, + nixpkgs, + flake-parts, + pre-commit-hooks, + ... + }: + flake-parts.lib.mkFlake {inherit inputs;} { + systems = ["x86_64-linux"]; + perSystem = { + config, + lib, + pkgs, + system, + ... + }: { + checks.pre-commit-check = pre-commit-hooks.lib.${system}.run { + src = ./.; + hooks = { + # Nix formatting + alejandra.enable = true; + + # Toml formatting + taplo.enable = true; + + # TODO: Python linting and formatting + + # Spell checking + typos = { + enable = true; + settings.ignored-words = ["hass"]; + }; + }; + }; + + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; + [ + python312 + python312Packages.pip + + just + ] + ++ [ + self.checks.${system}.pre-commit-check.enabledPackages + ]; + shellHook = '' + ${self.checks.${system}.pre-commit-check.shellHook} + + python -m venv .venv + source .venv/bin/activate + pip install -r requirements.dev.txt + ''; + }; + }; + }; +} diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..92498a0 --- /dev/null +++ b/hacs.json @@ -0,0 +1,3 @@ +{ + "name": "ouman_eh800" +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..36ff527 --- /dev/null +++ b/justfile @@ -0,0 +1,8 @@ +_default: + just --list + +hass-demo: + podman run --rm --name hass-demo -p 8123:8123 -v ${PWD}/.hass_dev:/config -v ${PWD}/custom_components:/config/custom_components --cap-add=CAP_NET_RAW,CAP_NET_BIND_SERVICE homeassistant/home-assistant:stable + +hass-demo-attach: + podman exec -it hass-demo bash diff --git a/requirements.dev.txt b/requirements.dev.txt new file mode 100644 index 0000000..2d65e9e --- /dev/null +++ b/requirements.dev.txt @@ -0,0 +1 @@ +homeassistant==2024.10.4 -- 2.44.1