DEVELOPMENT ENVIRONMENT

~liljamo/ha-ouman-eh800

a21df4529c0abd305baa27231561fa0a8b1ae0b2 — Jonni Liljamo 19 days ago
initial
A  => .envrc +1 -0
@@ 1,1 @@
use_flake

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