A => .envrc +1 -0
A => .gitignore +18 -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/*
A => .hass_dev/automations.yaml +1 -0
@@ 1,1 @@
+[]<
\ No newline at end of file
A => .hass_dev/configuration.yaml +16 -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
A => .hass_dev/scenes.yaml +0 -0
A => .hass_dev/scripts.yaml +0 -0
A => custom_components/ouman_eh800/__init__.py +24 -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
A => custom_components/ouman_eh800/config_flow.py +44 -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])
A => custom_components/ouman_eh800/const.py +15 -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_ = ""
A => custom_components/ouman_eh800/eh800.py +44 -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")
+
A => custom_components/ouman_eh800/manifest.json +6 -0
@@ 1,6 @@
+{
+ "domain": "ouman_eh800",
+ "name": "Ouman EH-800",
+ "config_flow": true,
+ "version": "0.1.0"
+}
A => custom_components/ouman_eh800/sensor.py +87 -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()
A => custom_components/ouman_eh800/strings.json +15 -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."
+ }
+ }
+ }
+ }
+}
A => custom_components/ouman_eh800/translations/en.json +1 -0
@@ 1,1 @@
+../strings.json<
\ No newline at end of file
A => flake.lock +125 -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
+}
A => flake.nix +71 -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
+ '';
+ };
+ };
+ };
+}
A => hacs.json +3 -0
@@ 1,3 @@
+{
+ "name": "ouman_eh800"
+}
A => justfile +8 -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
A => requirements.dev.txt +1 -0
@@ 1,1 @@
+homeassistant==2024.10.4