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