From 53ef7ae37ed8a8fe149b1ac36964f8e195dac670 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Sat, 9 Apr 2022 11:19:19 +0200 Subject: [PATCH] add config flow --- .devcontainer/configuration.yaml | 19 +- .devcontainer/devcontainer.json | 17 +- .gitignore | 2 + .pre-commit-config.yaml | 10 +- README.md | 96 +---- custom_components/bodymiscale/__init__.py | 398 +++++++----------- custom_components/bodymiscale/body_metrics.py | 20 +- custom_components/bodymiscale/config_flow.py | 133 ++++++ custom_components/bodymiscale/const.py | 76 ++-- custom_components/bodymiscale/coordinator.py | 199 +++++++++ custom_components/bodymiscale/entity.py | 65 +++ custom_components/bodymiscale/manifest.json | 3 +- custom_components/bodymiscale/strings.json | 9 - .../bodymiscale/translations/de.json | 32 ++ .../bodymiscale/translations/en.json | 32 +- .../bodymiscale/translations/fr.json | 3 +- .../bodymiscale/translations/pt-BR.json | 3 +- hacs.json | 3 +- requirements.txt | 8 +- 19 files changed, 703 insertions(+), 425 deletions(-) create mode 100644 custom_components/bodymiscale/config_flow.py create mode 100644 custom_components/bodymiscale/coordinator.py create mode 100644 custom_components/bodymiscale/entity.py delete mode 100644 custom_components/bodymiscale/strings.json create mode 100644 custom_components/bodymiscale/translations/de.json diff --git a/.devcontainer/configuration.yaml b/.devcontainer/configuration.yaml index 7c6b486..e9caa7b 100644 --- a/.devcontainer/configuration.yaml +++ b/.devcontainer/configuration.yaml @@ -15,16 +15,15 @@ input_number: max: 3000 step: 1 -bodymiscale: - test: - sensors: - weight: input_number.weight - impedance: input_number.impedance - height: 180 - born: "1990-01-01" - gender: "male" - model_miscale: "181B" +template: + - sensor: + - name: Weight + state: "{{ states('input_number.weight') }}" + unit_of_measurement: "kg" + - name: Impedance + state: "{{ states('input_number.impedance') }}" + unit_of_measurement: "ohm" # If you need to debug uncomment the line below (doc: https://www.home-assistant.io/integrations/debugpy/) debugpy: -# wait: true +# wait: true diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 450d708..1f6b8bd 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,10 +1,9 @@ -// See https://aka.ms/vscode-remote/devcontainer.json for format details. { - "image": "ludeeus/container:integration-debian", - "name": "Bodymiscale HA development", + "name": "Bodymiscale Development", + "image": "ghcr.io/ludeeus/devcontainer/integration:stable", + "postCreateCommand": "container install && pip install --ignore-installed -r requirements.txt", "context": "..", "appPort": ["9123:8123"], - "postCreateCommand": "container install", "extensions": [ "ms-python.python", "github.vscode-pull-request-github", @@ -14,11 +13,17 @@ "settings": { "files.eol": "\n", "editor.tabSize": 4, - "terminal.integrated.shell.linux": "/bin/bash", - "python.pythonPath": "/usr/bin/python3", + "terminal.integrated.profiles.linux": { + "zsh": { + "path": "/usr/bin/zsh" + } + }, + "terminal.integrated.defaultProfile.linux": "zsh", + "python.pythonPath": "/usr/local/python/bin/python", "python.analysis.autoSearchPaths": false, "python.linting.pylintEnabled": true, "python.linting.enabled": true, + "python.linting.pylintArgs": ["--disable", "import-error"], "python.formatting.provider": "black", "editor.formatOnPaste": false, "editor.formatOnSave": true, diff --git a/.gitignore b/.gitignore index f717486..0374119 100644 --- a/.gitignore +++ b/.gitignore @@ -123,3 +123,5 @@ dmypy.json # Pyre type checker .pyre/ + +.idea \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1ac02b9..1ea3567 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,12 +5,12 @@ ci: repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.31.0 + rev: v2.32.0 hooks: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/psf/black - rev: 22.1.0 + rev: 22.3.0 hooks: - id: black args: @@ -28,7 +28,7 @@ repos: <<: &python-files files: ^(custom_components/.+)?[^/]+\.py$ - repo: https://github.com/PyCQA/bandit - rev: 1.7.2 + rev: 1.7.4 hooks: - id: bandit args: @@ -41,7 +41,7 @@ repos: hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v4.2.0 hooks: - id: check-executables-have-shebangs - id: check-merge-conflict @@ -49,7 +49,7 @@ repos: - id: no-commit-to-branch - id: requirements-txt-fixer - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.5.1 + rev: v2.6.2 hooks: - id: prettier - repo: https://github.com/adrienverge/yamllint.git diff --git a/README.md b/README.md index 74b281f..cf3ffac 100644 --- a/README.md +++ b/README.md @@ -8,27 +8,13 @@ EN : -The purpose of this custom integration is to have additional information when weighing yourself with a Xiaomi Mi Scale (or any other smart scale). The input sensors for the custom integration are `weight` and optionally `impedance` (Mi Scale V2 only). You can use [ESPHome](https://esphome.io/) or [BLE monitor](https://github.com/custom-components/ble_monitor) to collect the required data via Bluetooth. The calculations are done in the `body_metrics.py` file. The configuration is in `bodymiscale.yaml` where you define `name`, `weight`, `height`, `age`, `gender` and `impedance` (only for Mi Scale V2). The name of the component should be bodymiscale.username. +The purpose of this custom integration is to have additional information when weighing yourself with a smart scale like Xiaomi Mi Scale. +For example you can use [ESPHome](https://esphome.io/) or [BLE monitor](https://github.com/custom-components/ble_monitor) to collect the required data via Bluetooth. Information about the unit of measurement. All calculations are made using the unit of measurement KG. If your scale is set in lbs don't worry Bodymiscale will convert for you. If you want to display your data in lbs you can use the card here [lovelace-body-miscale-card](https://github.com/dckiller51/lovelace-body-miscale-card). Just click on Convert kg to lbs. -FR : - -Le but de ce composant est d'avoir des informations supplémentaires lorsque l'on se pese avec une balance connectée Miscale Xiaomi. Actuellement le poids est envoyé sur Hassio avec un [ESPHome](https://esphome.io/) ou [BLE monitor](https://github.com/custom-components/ble_monitor). Le calculateur est le fichier `body_metrics.py`. La base de données est dans le fichier `bodymiscale.yaml` on y retrouve `name`, `le poids`, `la taille`, `l'age`, `le genre` et `l'impedance` (uniquement pour la Mi Scale V2). Le nom du composant devra être bodymiscale.username. - -Information concernant l'unité de mesure. Tous les calcules sont réalisés à l'aide de l'unité de mesure en KG. Si votre balance est paramétré en lbs ne vous soucié pas Bodymiscale convertira pour vous. Si vous souhaitez afficher vos données en lbs vous pouvez utiliser la card ici [lovelace-body-miscale-card](https://github.com/dckiller51/lovelace-body-miscale-card). Il vous suffit de cliquer sur Convertir les kg en lbs. - -DE : - -Der Zweck dieser benutzerdefinierten Integration besteht darin, beim Wiegen mit einer Xiaomi Mi-Waage (oder einer anderen intelligenten Waage) zusätzliche Informationen zu erhalten. Die Sensoren für die benutzerdefinierte Integration sind "Gewicht" und optional "Impedanz" (nur Mi Scale V2). Sie können [ESPHome](https://esphome.io/) oder [BLE monitor](https://github.com/custom-components/ble_monitor) verwenden, um die erforderlichen Daten über Bluetooth zu erhalten. Die Berechnungen erfolgen in der Datei `body_metrics.py`. Die Konfiguration befindet sich in `bodymiscale.yaml` wo Sie `name`, `weight`, `height`, `age`, `gender` und `impedance` definieren (nur für Mi Scale V2). Der Name der Komponente sollte bodymiscale.username lauten. - -Informationen zur Maßeinheit. Alle Berechnungen werden mit der Maßeinheit KG durchgeführt. Wenn Ihre Waage in lbs eingestellt ist, brauchen Sie sich keine Sorgen zu machen, Bodymiscale wird das für Sie umrechnen. Wenn Sie Ihre Daten in lbs anzeigen möchten, können Sie die Karte hier [lovelace-body-miscale-card](https://github.com/dckiller51/lovelace-body-miscale-card) verwenden. Klicken Sie einfach auf kg in lbs umwandeln. - The generated data is : -## For miscale (181D) - -- Model Miscale - Weight - Height - Years @@ -38,27 +24,16 @@ The generated data is : - Visceral fat - Ideal weight - Bmi label +- Lean body mass \* +- Body fat \* +- Water \* +- Bone mass \* +- Muscle mass \* +- Fat mass ideal \* +- Protein \* +- Body type \* -## For miscale 2 (181B) (with to impedance) - -- Model Miscale -- Weight -- Height -- Years -- Gender -- Bmi -- Basal metabolism -- Visceral fat -- Ideal weight -- Bmi label -- Lean body mass -- Body fat -- Water -- Bone mass -- Muscle mass -- Fat mass ideal -- Protein -- Body type +\*: When also the impedance sensor is configured --- @@ -79,53 +54,10 @@ The generated data is : ## Configuration -| key | type | description | -| :----------------------- | :--------------------------- | :------------------------------------------- | -| **plateform (Required)** | string | `bodymiscale` | -| **name (Required)** | string | Custom name for the sensor. `yourname` | -| **weight (Required)** | sensors / sensor.weight\_ | Your sensor returning your weight. | -| **impedance (Optional)** | sensors / sensor.impedance\_ | Your sensor returning your impedance. | -| **height (Required)** | number | Your height in cm. | -| **born (Required)** | string | Your birthday. `"YYYY-MM-DD"` | -| **gender (Required)** | string | female or male. `"male"` | -| **Model (Optional)** | string | Define the scale model.`"181D"` or `"181B"`. | - ---- - -## Example - -**Configuration YAML** [configuration.yaml] - -```yaml -bodymiscale: !include components/bodymiscale.yaml -``` - -Create a file in `/config/components/bodymiscale.yaml`. - -**Configuration with default settings:** [bodymiscale.yaml] - -```yaml -yourname: - sensors: - weight: sensor.weight_aurelien - height: 176 - born: "1990-04-10" - gender: "male" - model_miscale: "181D" -``` - -**Configuration with impedance (miscale2) settings:** [bodymiscale.yaml] +1. [![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=bodyscale) + Click on the button above or go to your HA Configuration (Settings) -> Devices & Services -> Add -> Bodymiscale. -```yaml -yourname: - sensors: - weight: sensor.weight_aurelien - impedance: sensor.impedance_aurelien - height: 176 - born: "1990-04-10" - gender: "male" - model_miscale: "181B" -``` +2. Insert the required data and select your input sensor for `weight` and optional `impedance`. --- diff --git a/custom_components/bodymiscale/__init__.py b/custom_components/bodymiscale/__init__.py index 1cb61d5..3eb3cd3 100644 --- a/custom_components/bodymiscale/__init__.py +++ b/custom_components/bodymiscale/__init__.py @@ -1,26 +1,22 @@ """Support for bodymiscale.""" import logging -from datetime import datetime, timedelta -from typing import Any, Optional +from typing import Any import homeassistant.helpers.config_validation as cv import voluptuous as vol -from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, - CONF_SENSORS, - STATE_OK, - STATE_PROBLEM, - STATE_UNAVAILABLE, - STATE_UNKNOWN, -) -from homeassistant.core import Event, HomeAssistant, State, callback -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import Entity +from awesomeversion import AwesomeVersion +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_NAME, CONF_SENSORS, STATE_OK, STATE_PROBLEM +from homeassistant.const import __version__ as HA_VERSION +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import StateType -from custom_components.bodymiscale.const import ( +from custom_components.bodymiscale.coordinator import BodyScaleCoordinator + +from .body_metrics import BodyMetricsImpedance +from .body_score import BodyScore +from .const import ( ATTR_AGE, ATTR_BMI, ATTR_BMILABEL, @@ -28,43 +24,31 @@ ATTR_BODY, ATTR_BODY_SCORE, ATTR_BONES, - ATTR_BORN, ATTR_FAT, ATTR_FATMASSTOGAIN, ATTR_FATMASSTOLOSE, - ATTR_GENDER, - ATTR_HEIGHT, ATTR_IDEAL, ATTR_LBM, ATTR_METABOLIC, - ATTR_MODEL, ATTR_MUSCLE, ATTR_PROBLEM, ATTR_PROTEIN, - ATTR_SENSORS, ATTR_VISCERAL, ATTR_WATER, - CONF_MAX_IMPEDANCE, - CONF_MAX_WEIGHT, - CONF_MIN_IMPEDANCE, - CONF_MIN_WEIGHT, + COMPONENT, + CONF_BIRTHDAY, + CONF_GENDER, + CONF_HEIGHT, CONF_SENSOR_IMPEDANCE, CONF_SENSOR_WEIGHT, - DEFAULT_MAX_IMPEDANCE, - DEFAULT_MAX_WEIGHT, - DEFAULT_MIN_IMPEDANCE, - DEFAULT_MIN_WEIGHT, - DEFAULT_MODEL, + COORDINATORS, DOMAIN, + MIN_REQUIRED_HA_VERSION, + PLATFORMS, PROBLEM_NONE, - READING_IMPEDANCE, - READING_WEIGHT, STARTUP_MESSAGE, - UNIT_POUNDS, ) - -from .body_metrics import BodyMetrics, BodyMetricsImpedance -from .body_score import BodyScore +from .entity import BodyScaleBaseEntity _LOGGER = logging.getLogger(__name__) @@ -78,259 +62,161 @@ BODYMISCALE_SCHEMA = vol.Schema( { vol.Required(CONF_SENSORS): vol.Schema(SCHEMA_SENSORS), - vol.Optional(CONF_MIN_WEIGHT, default=DEFAULT_MIN_WEIGHT): vol.Coerce(float), - vol.Optional(CONF_MAX_WEIGHT, default=DEFAULT_MAX_WEIGHT): vol.Coerce(float), - vol.Optional( - CONF_MIN_IMPEDANCE, default=DEFAULT_MIN_IMPEDANCE - ): cv.positive_int, - vol.Optional( - CONF_MAX_IMPEDANCE, default=DEFAULT_MAX_IMPEDANCE - ): cv.positive_int, - vol.Required(ATTR_HEIGHT): cv.positive_int, - vol.Required(ATTR_BORN): cv.string, - vol.Required(ATTR_GENDER): cv.string, - vol.Optional(ATTR_MODEL, default=DEFAULT_MODEL): cv.string, - } + vol.Required(CONF_HEIGHT): cv.positive_int, + vol.Required("born"): cv.string, + vol.Required(CONF_GENDER): cv.string, + }, + extra=vol.ALLOW_EXTRA, ) CONFIG_SCHEMA = vol.Schema( {DOMAIN: {cv.string: BODYMISCALE_SCHEMA}}, extra=vol.ALLOW_EXTRA ) -SCAN_INTERVAL = timedelta(seconds=30) +def is_ha_supported() -> bool: + """Return True, if current HA version is supported.""" + if AwesomeVersion(HA_VERSION) >= MIN_REQUIRED_HA_VERSION: + return True -async def async_setup(hass: HomeAssistant, config: dict) -> bool: - """Set up the Bodymiscale component.""" - if hass.data.get(DOMAIN) is None: - hass.data.setdefault(DOMAIN, {}) - _LOGGER.info(STARTUP_MESSAGE) - - component = EntityComponent(_LOGGER, DOMAIN, hass) + _LOGGER.error( + 'Unsupported HA version! Please upgrade home assistant at least to "%s"', + MIN_REQUIRED_HA_VERSION, + ) + return False - entities = [] - for bodymiscale_name, bodymiscale_config in config[DOMAIN].items(): - _LOGGER.info("Added bodymiscale %s", bodymiscale_name) - entity = Bodymiscale(bodymiscale_name, bodymiscale_config) - entities.append(entity) - - await component.async_add_entities(entities) - - return True +async def async_setup(hass: HomeAssistant, config: dict) -> bool: + """Set up component via yaml.""" + if DOMAIN in config: + if not is_ha_supported(): + return False + + _LOGGER.warning( + "Configuration of the bodymiscale in YAML is deprecated " + "and will be removed future versions; Your existing " + "configuration has been imported into the UI automatically and can be " + "safely removed from your configuration.yaml file" + ) -def _get_age(date: str) -> int: - born = datetime.strptime(date, "%Y-%m-%d") - today = datetime.today() - age = today.year - born.year - if (today.month, today.day) < (born.month, born.day): - age -= 1 - return age - + for name, conf in config[DOMAIN].items(): + conf[CONF_NAME] = name + conf[CONF_SENSOR_WEIGHT] = conf[CONF_SENSORS][CONF_SENSOR_WEIGHT] + if CONF_SENSOR_IMPEDANCE in conf[CONF_SENSORS]: + conf[CONF_SENSOR_IMPEDANCE] = conf[CONF_SENSORS][CONF_SENSOR_IMPEDANCE] -class Bodymiscale(Entity): # type: ignore - """Bodymiscale the well-being of a body. + del conf[CONF_SENSORS] - It also checks the measurements against weight, height, age, - gender and *impedance (*only miscale 2) - """ + conf[CONF_BIRTHDAY] = conf.pop("born") - READINGS = { - READING_WEIGHT: { - "min": CONF_MIN_WEIGHT, - "max": CONF_MAX_WEIGHT, - }, - READING_IMPEDANCE: { - "min": CONF_MIN_IMPEDANCE, - "max": CONF_MAX_IMPEDANCE, - }, - } + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + ) - def __init__(self, name: str, config: dict[str, Any]): - """Initialize the Bodymiscale component.""" - self._config = config - self._state: Optional[str] = None - self._name = name - self._problems = PROBLEM_NONE - self._weight: Optional[float] = None - self._impedance: Optional[int] = None - self._attr_height = self._config[ATTR_HEIGHT] - self._attr_born = self._config[ATTR_BORN] - self._attr_gender = self._config[ATTR_GENDER] - self._attr_model = self._config[ATTR_MODEL] - - @callback # type: ignore - def _state_changed_event(self, event: Event) -> None: - """Sensor state change event.""" - self._state_changed(event.data.get("entity_id"), event.data.get("new_state")) - - @callback # type: ignore - def _state_changed(self, entity_id: str, new_state: State) -> None: - """Update the sensor status.""" - if new_state is None: - return - value = new_state.state - _LOGGER.debug("Received callback from %s with value %s", entity_id, value) - if value == STATE_UNKNOWN: - return - - for sensor_type, sensor_entity_id in self._config[CONF_SENSORS].items(): - if entity_id != sensor_entity_id: - continue - - if value != STATE_UNAVAILABLE: - value = float(value) - - if sensor_type == CONF_SENSOR_WEIGHT: - if new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_POUNDS: - value = value * 0.45359237 - self._weight = value - else: - self._impedance = value - - self._update_state() - return - - raise HomeAssistantError(f"Unknown reading from sensor {entity_id}: {value}") - - def _update_state(self) -> None: - """Update the state of the class based sensor data.""" - result = [] - for sensor_type in self._config[CONF_SENSORS].keys(): - params = self.READINGS[sensor_type] - if (value := getattr(self, f"_{sensor_type}")) is not None: - if value == STATE_UNAVAILABLE: - result.append(f"{sensor_type} unavailable") - else: - if self._is_below_min(value, params): - result.append(f"{sensor_type} low") - if self._is_above_max(value, params): - result.append(f"{sensor_type} high") - - if result: - self._state = STATE_PROBLEM - self._problems = ", ".join(result) - else: - self._state = STATE_OK - self._problems = PROBLEM_NONE - _LOGGER.debug("New data processed") - self.async_write_ha_state() + return True - def _is_below_min(self, value: float, params: dict[str, str]) -> bool: - """If configured, check the value against the defined minimum value.""" - if "min" in params and params["min"] in self._config: - min_value = self._config[params["min"]] - if value < min_value: - return True +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up component via UI.""" + if not is_ha_supported(): return False - def _is_above_max(self, value: float, params: dict[str, str]) -> bool: - """If configured, check the value against the defined maximum value.""" - if "max" in params and params["max"] in self._config: - max_value = self._config[params["max"]] - if value > max_value: - return True + if hass.data.get(DOMAIN) is None: + hass.data.setdefault( + DOMAIN, + { + COMPONENT: EntityComponent(_LOGGER, DOMAIN, hass), + COORDINATORS: {}, + }, + ) + _LOGGER.info(STARTUP_MESSAGE) - return False + coordinator = hass.data[DOMAIN][COORDINATORS][ + entry.entry_id + ] = BodyScaleCoordinator(hass, entry.data) - async def async_added_to_hass(self) -> None: - """After being added to hass.""" + component = hass.data[DOMAIN][COMPONENT] + await component.async_add_entities([Bodymiscale(coordinator)]) - async_track_state_change_event( - self.hass, - list(self._config[CONF_SENSORS].values()), - self._state_changed_event, - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True - for entity_id in self._config[CONF_SENSORS].values(): - if (state := self.hass.states.get(entity_id)) is not None: - self._state_changed(entity_id, state) - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False +class Bodymiscale(BodyScaleBaseEntity): + """Bodymiscale the well-being of a body. - @property - def name(self) -> str: - """Return the name of the sensor.""" - return self._name + It also checks the measurements against weight, height, age, + gender and impedance (if configured). + """ - @property - def icon(self) -> str: - """Return the icon that will be shown in the interface.""" - return "mdi:human" + def __init__(self, coordinator: BodyScaleCoordinator): + """Initialize the Bodymiscale component.""" + super().__init__( + coordinator, + EntityDescription( + key="Bodymiscale", name=coordinator.config[CONF_NAME], icon="mdi:human" + ), + ) - @property - def state(self) -> StateType: - """Return the state of the entity.""" - return self._state + def _on_update(self) -> None: + """Perform actions on update.""" + if self._coordinator.problems == PROBLEM_NONE: + self._attr_state = STATE_OK + else: + self._attr_state = STATE_PROBLEM + self.async_write_ha_state() @property def state_attributes(self) -> dict[str, Any]: """Return the attributes of the entity.""" - age = _get_age(self._attr_born) - problem = self._state - problem_sensor = self._problems attrib = { - ATTR_PROBLEM: self._problems, - ATTR_SENSORS: self._config[CONF_SENSORS], - ATTR_MODEL: self._attr_model, - ATTR_HEIGHT: f"{self._attr_height}", - ATTR_GENDER: self._attr_gender, - ATTR_AGE: f"{age}", + ATTR_PROBLEM: self._coordinator.problems, + CONF_HEIGHT: self._coordinator.config[CONF_HEIGHT], + CONF_GENDER: self._coordinator.config[CONF_GENDER].value, + ATTR_AGE: self._coordinator.config[ATTR_AGE], + CONF_SENSOR_WEIGHT: self._coordinator.weight, } - for reading in self._config[CONF_SENSORS].keys(): - attrib[reading] = getattr(self, f"_{reading}") - - if self._weight is None or "unavailable" in problem_sensor or problem != "ok": - return attrib - - metrics = BodyMetrics(self._weight, self._attr_height, age, self._attr_gender) - - if ( - self._attr_model == "181B" - and "impedance" not in problem_sensor - and self._impedance is not None - ): - metrics = BodyMetricsImpedance( - self._weight, self._attr_height, age, self._attr_gender, self._impedance - ) - - attrib[ATTR_BMI] = f"{metrics.bmi:.1f}" - attrib[ATTR_BMR] = f"{metrics.bmr:.0f}" - attrib[ATTR_VISCERAL] = f"{metrics.visceral_fat:.0f}" - attrib[ATTR_IDEAL] = f"{metrics.ideal_weight:.2f}" - attrib[ATTR_BMILABEL] = metrics.bmi_label - - if isinstance(metrics, BodyMetricsImpedance): - bodyscale = [ - "Obese", - "Overweight", - "Thick-set", - "Lack-exercise", - "Balanced", - "Balanced-muscular", - "Skinny", - "Balanced-skinny", - "Skinny-muscular", - ] - attrib[ATTR_LBM] = f"{metrics.lbm_coefficient:.1f}" - attrib[ATTR_FAT] = f"{metrics.fat_percentage:.1f}" - attrib[ATTR_WATER] = f"{metrics.water_percentage:.1f}" - attrib[ATTR_BONES] = f"{metrics.bone_mass:.2f}" - attrib[ATTR_MUSCLE] = f"{metrics.muscle_mass:.2f}" - fat_mass_to_ideal = metrics.fat_mass_to_ideal - if fat_mass_to_ideal["type"] == "to_lose": - attrib[ATTR_FATMASSTOLOSE] = f"{fat_mass_to_ideal['mass']:.2f}" - else: - attrib[ATTR_FATMASSTOGAIN] = f"{fat_mass_to_ideal['mass']:.2f}" - attrib[ATTR_PROTEIN] = f"{metrics.protein_percentage:.1f}" - attrib[ATTR_BODY] = bodyscale[metrics.body_type] - attrib[ATTR_METABOLIC] = f"{metrics.metabolic_age:.0f}" - body_score = BodyScore(metrics) - attrib[ATTR_BODY_SCORE] = f"{body_score.body_score:.0f}" + if CONF_SENSOR_IMPEDANCE in self._coordinator.config: + attrib[CONF_SENSOR_IMPEDANCE] = self._coordinator.impedance + + metrics = self._coordinator.metrics + + if metrics: + attrib[ATTR_BMI] = f"{metrics.bmi:.1f}" + attrib[ATTR_BMR] = f"{metrics.bmr:.0f}" + attrib[ATTR_VISCERAL] = f"{metrics.visceral_fat:.0f}" + attrib[ATTR_IDEAL] = f"{metrics.ideal_weight:.2f}" + attrib[ATTR_BMILABEL] = metrics.bmi_label + + if isinstance(metrics, BodyMetricsImpedance): + bodyscale = [ + "Obese", + "Overweight", + "Thick-set", + "Lack-exercise", + "Balanced", + "Balanced-muscular", + "Skinny", + "Balanced-skinny", + "Skinny-muscular", + ] + attrib[ATTR_LBM] = f"{metrics.lbm_coefficient:.1f}" + attrib[ATTR_FAT] = f"{metrics.fat_percentage:.1f}" + attrib[ATTR_WATER] = f"{metrics.water_percentage:.1f}" + attrib[ATTR_BONES] = f"{metrics.bone_mass:.2f}" + attrib[ATTR_MUSCLE] = f"{metrics.muscle_mass:.2f}" + fat_mass_to_ideal = metrics.fat_mass_to_ideal + if fat_mass_to_ideal["type"] == "to_lose": + attrib[ATTR_FATMASSTOLOSE] = f"{fat_mass_to_ideal['mass']:.2f}" + else: + attrib[ATTR_FATMASSTOGAIN] = f"{fat_mass_to_ideal['mass']:.2f}" + attrib[ATTR_PROTEIN] = f"{metrics.protein_percentage:.1f}" + attrib[ATTR_BODY] = bodyscale[metrics.body_type] + attrib[ATTR_METABOLIC] = f"{metrics.metabolic_age:.0f}" + body_score = BodyScore(metrics) + attrib[ATTR_BODY_SCORE] = f"{body_score.body_score:.0f}" return attrib diff --git a/custom_components/bodymiscale/body_metrics.py b/custom_components/bodymiscale/body_metrics.py index e3301b2..cc24e0e 100644 --- a/custom_components/bodymiscale/body_metrics.py +++ b/custom_components/bodymiscale/body_metrics.py @@ -3,6 +3,12 @@ from typing import Union from .body_scales import BodyScale +from .const import ( + CONSTRAINT_HEIGHT_MAX, + CONSTRAINT_IMPEDANCE_MAX, + CONSTRAINT_WEIGHT_MAX, + CONSTRAINT_WEIGHT_MIN, +) from .models import Gender @@ -20,11 +26,11 @@ class BodyMetrics: def __init__(self, weight: float, height: int, age: int, gender: Gender): # Check for potential out of boundaries - if height > 220: - raise Exception("Height is too high (limit: >220cm)") - if weight < 10 or weight > 200: + if height > CONSTRAINT_HEIGHT_MAX: + raise Exception(f"Height is too high (limit: {CONSTRAINT_HEIGHT_MAX}cm)") + if not (CONSTRAINT_WEIGHT_MIN < weight < CONSTRAINT_WEIGHT_MAX): raise Exception( - "Weight is either too low or too high (limits: <10kg and >200kg)" + f"Weight not within {CONSTRAINT_WEIGHT_MIN} and {CONSTRAINT_WEIGHT_MAX} kg" ) if age > 99: raise Exception("Age is too high (limit >99 years)") @@ -149,8 +155,10 @@ def __init__( # pylint: disable=too-many-arguments self, weight: float, height: int, age: int, gender: Gender, impedance: int ): super().__init__(weight, height, age, gender) - if impedance > 3000: - raise Exception("Impedance is too high (limit >3000ohm)") + if impedance > CONSTRAINT_IMPEDANCE_MAX: + raise Exception( + f"Impedance is too high (limit >{CONSTRAINT_IMPEDANCE_MAX}ohm)" + ) self._impedance = impedance self._scale = BodyScale(age, height, gender, weight) diff --git a/custom_components/bodymiscale/config_flow.py b/custom_components/bodymiscale/config_flow.py new file mode 100644 index 0000000..3b508a1 --- /dev/null +++ b/custom_components/bodymiscale/config_flow.py @@ -0,0 +1,133 @@ +"""Config flow to configure the bodymiscale integration.""" +from __future__ import annotations + +from types import MappingProxyType +from typing import Any + +import voluptuous as vol +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.const import CONF_MODE, CONF_NAME, CONF_UNIT_OF_MEASUREMENT +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.selector import selector + +from .const import ( + CONF_BIRTHDAY, + CONF_GENDER, + CONF_HEIGHT, + CONF_SENSOR_IMPEDANCE, + CONF_SENSOR_WEIGHT, + CONSTRAINT_HEIGHT_MAX, + CONSTRAINT_HEIGHT_MIN, + DOMAIN, + MAX, + MIN, +) +from .models import Gender + + +@callback # type: ignore[misc] +def _get_schema( + defaults: dict[str, Any] | MappingProxyType[str, Any], is_options_handler: bool +) -> vol.Schema: + """Return bodymiscale schema.""" + + schema = { + vol.Required( + CONF_HEIGHT, default=defaults.get(CONF_HEIGHT, vol.UNDEFINED) + ): selector( + { + "number": { + MIN: CONSTRAINT_HEIGHT_MIN, + MAX: CONSTRAINT_HEIGHT_MAX, + CONF_UNIT_OF_MEASUREMENT: "cm", + CONF_MODE: "box", + } + } + ), + vol.Required( + CONF_SENSOR_WEIGHT, default=defaults.get(CONF_SENSOR_WEIGHT, vol.UNDEFINED) + ): selector({"entity": {"domain": "sensor"}}), + vol.Optional( + CONF_SENSOR_IMPEDANCE, + default=defaults.get(CONF_SENSOR_IMPEDANCE, vol.UNDEFINED), + ): selector({"entity": {"domain": "sensor"}}), + } + + if not is_options_handler: + schema = { + vol.Required( + CONF_NAME, default=defaults.get(CONF_NAME, vol.UNDEFINED) + ): str, + vol.Required( + CONF_BIRTHDAY, default=defaults.get(CONF_BIRTHDAY, vol.UNDEFINED) + ): selector({"date": {}}), + vol.Required( + CONF_GENDER, default=defaults.get(CONF_GENDER, vol.UNDEFINED) + ): vol.In({gender: gender.value for gender in Gender}), + **schema, + } + + return vol.Schema(schema) + + +class BodyMiScaleFlowHandler(ConfigFlow, domain=DOMAIN): # type: ignore[misc, call-arg] + """Config flow for bodymiscale.""" + + VERSION = 1 + + @staticmethod + @callback # type: ignore[misc] + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> BodyMiScaleOptionsFlowHandler: + """Get the options flow for this handler.""" + return BodyMiScaleOptionsFlowHandler(config_entry) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + if user_input is not None: + return self._create_entry(user_input) + + user_input = {} + return self.async_show_form( + step_id="user", + data_schema=_get_schema(user_input, is_options_handler=False), + ) + + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + """Handle a flow initialized by importing a config.""" + return self._create_entry(config) + + def _create_entry(self, config: dict[str, Any]) -> FlowResult: + self._async_abort_entries_match({CONF_NAME: config[CONF_NAME]}) + + return self.async_create_entry(title=config[CONF_NAME], data=config) + + +class BodyMiScaleOptionsFlowHandler(OptionsFlow): # type: ignore[misc] + """Handle Body mi scale options.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize Body mi scale options flow.""" + self._config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage Body mi scale options.""" + + if user_input is not None: + return self.async_create_entry( + title=self._config_entry.title, + data={**self._config_entry.data, **user_input}, + ) + + user_input = self._config_entry.data + + return self.async_show_form( + step_id="init", + data_schema=_get_schema(user_input, is_options_handler=True), + ) diff --git a/custom_components/bodymiscale/const.py b/custom_components/bodymiscale/const.py index 6633bfa..a087fb5 100644 --- a/custom_components/bodymiscale/const.py +++ b/custom_components/bodymiscale/const.py @@ -1,61 +1,43 @@ """Constants for bodymiscale.""" -# Base component constants -NAME = "Body Xiaomi Miscale Esphome" + +from homeassistant.const import Platform + +MIN_REQUIRED_HA_VERSION = "2022.4.0b0" +NAME = "BodyMiScale" DOMAIN = "bodymiscale" VERSION = "2.0.0" ISSUE_URL = "https://github.com/dckiller51/bodymiscale/issues" -DOC_URL = "https://github.com/dckiller51/bodymiscale" -# Icons -ICON = "mdi:human" +CONF_BIRTHDAY = "birthday" +CONF_GENDER = "gender" +CONF_HEIGHT = "height" +CONF_SENSOR_IMPEDANCE = "impedance" +CONF_SENSOR_WEIGHT = "weight" -# Common constants Miscale -READING_WEIGHT = "weight" -CONF_SENSOR_WEIGHT = READING_WEIGHT -CONF_MIN_WEIGHT = f"min_{READING_WEIGHT}" -CONF_MAX_WEIGHT = f"max_{READING_WEIGHT}" -ATTR_HEIGHT = "height" -ATTR_BORN = "born" -ATTR_GENDER = "gender" ATTR_AGE = "age" ATTR_BMI = "bmi" +ATTR_BMILABEL = "bmi_label" ATTR_BMR = "basal_metabolism" -ATTR_VISCERAL = "visceral_fat" +ATTR_BODY = "body_type" +ATTR_BODY_SCORE = "body_score" +ATTR_BONES = "bone_mass" +ATTR_FAT = "body_fat" +ATTR_FATMASSTOGAIN = "fat_mass_to_gain" +ATTR_FATMASSTOLOSE = "fat_mass_to_lose" ATTR_IDEAL = "ideal" -ATTR_BMILABEL = "bmi_label" - -# Constants for Miscale 2 -READING_IMPEDANCE = "impedance" -CONF_SENSOR_IMPEDANCE = READING_IMPEDANCE -CONF_MIN_IMPEDANCE = f"min_{READING_IMPEDANCE}" -CONF_MAX_IMPEDANCE = f"max_{READING_IMPEDANCE}" ATTR_LBM = "lean_body_mass" -ATTR_FAT = "body_fat" -ATTR_WATER = "water" -ATTR_BONES = "bone_mass" +ATTR_METABOLIC = "metabolic_age" ATTR_MUSCLE = "muscle_mass" -ATTR_FATMASSTOLOSE = "fat_mass_to_lose" -ATTR_FATMASSTOGAIN = "fat_mass_to_gain" +ATTR_PROBLEM = "problem" ATTR_PROTEIN = "protein" -ATTR_BODY = "body_type" -ATTR_BODY_SCORE = "body_score" -ATTR_METABOLIC = "metabolic_age" +ATTR_VISCERAL = "visceral_fat" +ATTR_WATER = "water" + UNIT_POUNDS = "lbs" -# Defaults -ATTR_PROBLEM = "problem" -ATTR_SENSORS = "sensors" PROBLEM_NONE = "none" -ATTR_MODEL = "model_miscale" - -DEFAULT_MIN_WEIGHT = 10 -DEFAULT_MAX_WEIGHT = 200 -DEFAULT_MIN_IMPEDANCE = 0 -DEFAULT_MAX_IMPEDANCE = 3000 -DEFAULT_MODEL = "181D" - STARTUP_MESSAGE = f""" ------------------------------------------------------------------- {NAME} @@ -65,3 +47,17 @@ {ISSUE_URL} ------------------------------------------------------------------- """ + +CONSTRAINT_HEIGHT_MIN = 0 +CONSTRAINT_HEIGHT_MAX = 220 +CONSTRAINT_IMPEDANCE_MIN = 0 +CONSTRAINT_IMPEDANCE_MAX = 3000 +CONSTRAINT_WEIGHT_MIN = 10 +CONSTRAINT_WEIGHT_MAX = 200 + +MIN = "min" +MAX = "max" +COMPONENT = "component" +COORDINATORS = "coordinators" + +PLATFORMS: set[Platform] = set() diff --git a/custom_components/bodymiscale/coordinator.py b/custom_components/bodymiscale/coordinator.py new file mode 100644 index 0000000..5d35058 --- /dev/null +++ b/custom_components/bodymiscale/coordinator.py @@ -0,0 +1,199 @@ +"""Coordinator module.""" +import logging +from datetime import datetime +from typing import Any, Optional, Union + +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, State, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.event import async_track_state_change_event + +from .body_metrics import BodyMetrics, BodyMetricsImpedance +from .const import ( + ATTR_AGE, + CONF_BIRTHDAY, + CONF_GENDER, + CONF_HEIGHT, + CONF_SENSOR_IMPEDANCE, + CONF_SENSOR_WEIGHT, + CONSTRAINT_IMPEDANCE_MAX, + CONSTRAINT_IMPEDANCE_MIN, + CONSTRAINT_WEIGHT_MAX, + CONSTRAINT_WEIGHT_MIN, + MAX, + MIN, + PROBLEM_NONE, + UNIT_POUNDS, +) +from .models import Gender + +_LOGGER = logging.getLogger(__name__) + + +def _get_age(date: str) -> int: + born = datetime.strptime(date, "%Y-%m-%d") + today = datetime.today() + age = today.year - born.year + if (today.month, today.day) < (born.month, born.day): + age -= 1 + return age + + +class BodyScaleCoordinator: + """Body scale coordinator.""" + + READINGS = { + CONF_SENSOR_WEIGHT: { + MIN: CONSTRAINT_WEIGHT_MIN, + MAX: CONSTRAINT_WEIGHT_MAX, + }, + CONF_SENSOR_IMPEDANCE: { + MIN: CONSTRAINT_IMPEDANCE_MIN, + MAX: CONSTRAINT_IMPEDANCE_MAX, + }, + } + + def __init__(self, hass: HomeAssistant, config: dict[str, Any]): + self._hass = hass + self._config: dict[str, Any] = { + **config, + ATTR_AGE: _get_age(config[CONF_BIRTHDAY]), + CONF_GENDER: Gender(config[CONF_GENDER]), + } + self._subscriptions: list[CALLBACK_TYPE] = [] + self._remove_listener: Optional[CALLBACK_TYPE] = None + + self._problems = PROBLEM_NONE + self._weight: Optional[float] = None + self._impedance: Optional[int] = None + + def subscribe(self, callback_func: CALLBACK_TYPE) -> CALLBACK_TYPE: + """Subscribe for changes.""" + self._subscriptions.append(callback_func) + + if len(self._subscriptions) == 1: + sensors = [self._config[CONF_SENSOR_WEIGHT]] + if CONF_SENSOR_IMPEDANCE in self._config: + sensors.append(self._config[CONF_SENSOR_IMPEDANCE]) + + self._remove_listener = async_track_state_change_event( + self._hass, + sensors, + self._state_changed_event, + ) + + for entity_id in sensors: + if (state := self._hass.states.get(entity_id)) is not None: + self._state_changed(entity_id, state) + + @callback # type: ignore[misc] + def remove_listener() -> None: + """Remove subscribtion.""" + self._subscriptions.remove(callback_func) + + if len(self._subscriptions) == 0 and self._remove_listener: + self._remove_listener() + + return remove_listener + + @callback # type: ignore[misc] + def _state_changed_event(self, event: Event) -> None: + """Sensor state change event.""" + self._state_changed(event.data.get("entity_id"), event.data.get("new_state")) + + @callback # type: ignore[misc] + def _state_changed(self, entity_id: str, new_state: State) -> None: + """Update the sensor status.""" + if new_state is None: + return + + value = new_state.state + _LOGGER.debug("Received callback from %s with value %s", entity_id, value) + if value == STATE_UNKNOWN: + return + + if value != STATE_UNAVAILABLE: + value = float(value) + + if entity_id == self._config[CONF_SENSOR_WEIGHT]: + if new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_POUNDS: + value = value * 0.45359237 + self._weight = value + elif entity_id == self._config.get(CONF_SENSOR_IMPEDANCE, None): + self._impedance = value + else: + raise HomeAssistantError( + f"Unknown reading from sensor {entity_id}: {value}" + ) + + self._update_state() + + def _update_state(self) -> None: + """Update the state of the class based sensor data.""" + problems = [] + sensor_types = [CONF_SENSOR_WEIGHT] + if CONF_SENSOR_IMPEDANCE in self._config: + sensor_types.append(CONF_SENSOR_IMPEDANCE) + + for sensor_type in sensor_types: + params = self.READINGS[sensor_type] + if (value := getattr(self, f"_{sensor_type}")) is not None: + if value == STATE_UNAVAILABLE: + problems.append(f"{sensor_type} unavailable") + else: + if value < params[MIN]: + problems.append(f"{sensor_type} low") + elif value > params[MAX]: + problems.append(f"{sensor_type} high") + + if problems: + self._problems = ", ".join(problems) + else: + self._problems = PROBLEM_NONE + + _LOGGER.debug("New data processed") + for sub_callback in self._subscriptions: + sub_callback() + + @property + def config(self) -> dict[str, Any]: + """Return config.""" + return self._config + + @property + def problems(self) -> str: + """Return problems.""" + return self._problems + + @property + def weight(self) -> Optional[float]: + """Return weight.""" + return self._weight + + @property + def impedance(self) -> Optional[float]: + """Return impedance.""" + return self._impedance + + @property + def metrics(self) -> Union[BodyMetrics, BodyMetricsImpedance, None]: + """Return metrics object.""" + if CONF_SENSOR_WEIGHT in self._problems or self._weight is None: + return None + + height: int = self._config[CONF_HEIGHT] + gender: Gender = self._config[CONF_GENDER] + age = self._config[ATTR_AGE] + + metrics = BodyMetrics(self._weight, height, age, gender) + + if CONF_SENSOR_IMPEDANCE not in self._problems and self._impedance is not None: + metrics = BodyMetricsImpedance( + self._weight, height, age, gender, self._impedance + ) + + return metrics diff --git a/custom_components/bodymiscale/entity.py b/custom_components/bodymiscale/entity.py new file mode 100644 index 0000000..4a3d616 --- /dev/null +++ b/custom_components/bodymiscale/entity.py @@ -0,0 +1,65 @@ +"""Bodymiscale entity module.""" +from abc import abstractmethod +from typing import Optional + +from homeassistant.const import CONF_NAME +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription + +from .const import DOMAIN, NAME, VERSION +from .coordinator import BodyScaleCoordinator + + +class BodyScaleBaseEntity(Entity): # type: ignore[misc] + """Body scale base entity.""" + + _attr_should_poll = False + + def __init__( + self, + coordinator: BodyScaleCoordinator, + entity_description: Optional[EntityDescription] = None, + ): + """Initialize the entity.""" + super().__init__() + self._coordinator = coordinator + if entity_description: + self.entity_description = entity_description + elif not hasattr(self, "entity_description"): + raise ValueError( + '"entity_description" must be either set as class variable or passed on init!' + ) + + if not self.entity_description.key: + raise ValueError('"entity_description.key" must be either set!') + + name = coordinator.config[CONF_NAME] + self._attr_unique_id = "_".join([DOMAIN, name, self.entity_description.key]) + + if self.entity_description.name: + # Name provided, using the provided one + if not self.entity_description.name.lower().startswith(name.lower()): + # Entity name should start with configurated name + self._attr_name = f"{name} {self.entity_description.name}" + else: + self._attr_name = f"{name} {self.entity_description.key.replace('_', ' ')}" + + @property + def device_info(self) -> Optional[DeviceInfo]: + """Return device specific attributes.""" + return DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + name=NAME, + sw_version=VERSION, + ) + + @abstractmethod + def _on_update(self) -> None: + """Perform actions on update.""" + raise NotImplementedError + + async def async_added_to_hass(self) -> None: + """After being added to hass.""" + await super().async_added_to_hass() + + self.async_on_remove(self._coordinator.subscribe(self._on_update)) diff --git a/custom_components/bodymiscale/manifest.json b/custom_components/bodymiscale/manifest.json index 70c0543..f414bcc 100644 --- a/custom_components/bodymiscale/manifest.json +++ b/custom_components/bodymiscale/manifest.json @@ -7,5 +7,6 @@ "codeowners": ["@dckiller51"], "requirements": [], "iot_class": "calculated", - "version": "2.0.0" + "version": "2.0.0", + "config_flow": true } diff --git a/custom_components/bodymiscale/strings.json b/custom_components/bodymiscale/strings.json deleted file mode 100644 index 3d13f75..0000000 --- a/custom_components/bodymiscale/strings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "title": "Bodymiscale", - "state": { - "_": { - "ok": "[%key:component::binary_sensor::state::problem::off%]", - "problem": "[%key:component::binary_sensor::state::problem::on%]" - } - } -} diff --git a/custom_components/bodymiscale/translations/de.json b/custom_components/bodymiscale/translations/de.json new file mode 100644 index 0000000..e253435 --- /dev/null +++ b/custom_components/bodymiscale/translations/de.json @@ -0,0 +1,32 @@ +{ + "config": { + "step": { + "user": { + "data": { + "height": "Größe", + "birthday": "Geburtstag", + "gender": "Geschlecht", + "weight": "Gewicht sensor", + "impedance": "Impedanz sensor" + }, + "data_description": { + "impedance": "Falls deine Waage keine Impedanz liefert, lasse das Feld leer." + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "height": "Größe", + "weight": "Gewicht sensor", + "impedance": "Impedanz sensor" + }, + "data_description": { + "impedance": "Falls deine Waage keine Impedanz liefert, lasse das Feld leer." + } + } + } + } +} diff --git a/custom_components/bodymiscale/translations/en.json b/custom_components/bodymiscale/translations/en.json index 5eebff0..1f0d213 100644 --- a/custom_components/bodymiscale/translations/en.json +++ b/custom_components/bodymiscale/translations/en.json @@ -5,5 +5,35 @@ "problem": "Problem" } }, - "title": "Bodymiscale" + "config": { + "step": { + "user": { + "data": { + "name": "Name", + "height": "Height", + "birthday": "Birthday", + "gender": "Gender", + "weight": "Weight sensor", + "impedance": "Impedance sensor" + }, + "data_description": { + "impedance": "If your scale does not provide the impedance, leave this field empty." + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "height": "Height", + "weight": "Weight sensor", + "impedance": "Impedance sensor" + }, + "data_description": { + "impedance": "If your scale does not provide the impedance, leave this field empty." + } + } + } + } } diff --git a/custom_components/bodymiscale/translations/fr.json b/custom_components/bodymiscale/translations/fr.json index 77b1a14..90022e6 100644 --- a/custom_components/bodymiscale/translations/fr.json +++ b/custom_components/bodymiscale/translations/fr.json @@ -4,6 +4,5 @@ "ok": "OK", "problem": "Probl\u00e8me" } - }, - "title": "Bodymiscale" + } } diff --git a/custom_components/bodymiscale/translations/pt-BR.json b/custom_components/bodymiscale/translations/pt-BR.json index 6b799f5..457ca22 100644 --- a/custom_components/bodymiscale/translations/pt-BR.json +++ b/custom_components/bodymiscale/translations/pt-BR.json @@ -4,6 +4,5 @@ "ok": "OK", "problem": "Problema" } - }, - "title": "Bodymiscale" + } } diff --git a/hacs.json b/hacs.json index fd4dc64..0d097a9 100644 --- a/hacs.json +++ b/hacs.json @@ -1,4 +1,5 @@ { "name": "Bodymiscale", - "render_readme": true + "render_readme": true, + "homeassistant": "2022.4.0b0" } diff --git a/requirements.txt b/requirements.txt index 9f5278a..057bc86 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -homeassistant>=2022.3.0.b0 -mypy==0.931 -pre-commit==2.17.0 -pylint==2.12.2 +homeassistant>=2022.4.0.b0 +mypy==0.942 +pre-commit==2.18.1 +pylint==2.13.5 #pytest-homeassistant-custom-component==0.4.4