From 1d71383686aa21cd19ad57460c7defa447efd980 Mon Sep 17 00:00:00 2001 From: Ben Lebherz Date: Sun, 22 Nov 2020 19:19:36 +0100 Subject: [PATCH] remove translations and add customized messages --- README.md | 20 +++++- apps/notifreeze/notifreeze.py | 113 +++++++++++++++++++++------------- 2 files changed, 88 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 53b91ec..b3cfbef 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,8 @@ key | optional | type | default | description `max_difference` | True | float | 5 | Maximum tolerated tmperature difference `rooms` | False | list | | List of [**rooms**](#room) or simple *room* names NotiFreeze will monitor. Users of the famous [AutoMoLi](https://github.com/benleb/ad-automoli) may already by familiar with the *rooms* concept. `delays` | True | [**delay**](#delays) | [**see below**](#delays) | Delays NotiFreeze will use. -`locale` | True | string | `en_US` | Locale for notifications in native language. See bottom of [`notifreeze.py`](apps/notifreeze/notifreeze.py) for available ones or add one yourself +`messages` | True | [**message**](#messages) | default english | Custom notification messages +~~`locale`~~ | ~~True~~ | ~~string~~ | ~~`en_US`~~ | **replaced by `messages`** ~~Locale for notifications in native language. See bottom of [`notifreeze.py`](apps/notifreeze/notifreeze.py) for available ones or add one yourself~~ ## room @@ -80,6 +81,23 @@ key | optional | type | default | description `indoor` | True | string, list[string] | | Temperature sensor Entity ID(s) `door_window` | True | string, list[string] | | Door/Windows sensor Entity ID(s) +## messages + +key | optional | type | default | description +-- | -- | -- | -- | -- +`since` | True | string | {room_name} {entity_name} open since {open_since}: {initial}°C | sent when temperature **did not change** since the door/windows was opened +`change` | True | string | {room_name} {entity_name} open since {open_since}: {initial}°C → {indoor}°C ({indoor_difference}°C) | sent when temperature **has changed** since the door/windows was opened + +### variables + +var | description | not available in message +-- | -- | -- +`room_name` | name of the room +`entity_name` | name of the door/windows +`open_since` | time since opened +`initial` | indoor temperature when door/windows was opened +`indoor` | current indoor temperature | `since` + ## delays key | optional | type | default | description diff --git a/apps/notifreeze/notifreeze.py b/apps/notifreeze/notifreeze.py index 5961c35..24cf4ea 100644 --- a/apps/notifreeze/notifreeze.py +++ b/apps/notifreeze/notifreeze.py @@ -9,8 +9,8 @@ import re from datetime import datetime -from pprint import pformat from pathlib import PurePath +from pprint import pformat from statistics import fmean from sys import version_info from typing import Any, Dict, Iterable, List, Optional, Set, Union @@ -21,11 +21,6 @@ APP_NAME = "NotiFreeze" APP_ICON = "❄️ " -# state set by home assistant if entity exists but has no state -STATE_UNKNOWN = "unknown" - -SECONDS_PER_MIN: int = 60 - # default values DEFAULT_MAX_DIFFERENCE = 5.0 DEFAULT_INITIAL = 5 @@ -34,6 +29,21 @@ KEYWORD_DOOR_WINDOW = "binary_sensor.door_window_" KEYWORD_TEMPERATURE = "sensor.temperature_" +# translations +MSGS: Dict[str, Dict[str, str]] = { + "en_US": { + "since": "{room_name} {entity_name} open since {open_since}: {initial}°C", + "change": "{room_name} {entity_name} open since {open_since}: {initial}°C → {indoor}°C ({indoor_difference}°C)", + }, + "de_DE": { + "since": "{room_name} {entity_name} offen seit {open_since}: {initial}°C", + "change": "{room_name} {entity_name} offen seit {open_since}: {initial}°C → {indoor}°C ({indoor_difference}°C)", + }, +} + +# helper +SECONDS_PER_MIN: int = 60 + # version checks py3_or_higher = version_info.major >= 3 py37_or_higher = py3_or_higher and version_info.minor >= 7 @@ -91,17 +101,26 @@ def __init__( # reminder notification callback handles self.handles: Dict[str, str] = {} - async def indoor(self, nf: Any) -> float: - if indoor := [float(await nf.get_state(sensor)) for sensor in self.temperature]: - return fmean(indoor) - else: - raise ValueError + async def indoor(self, nf: Any) -> Optional[float]: + indoor_temperatures = set() + invalid_sensors = {} - async def difference(self, outdoor: float, nf: Any) -> float: - if indoor := await self.indoor(nf): - return round(outdoor - indoor, 2) - else: - raise ValueError + for sensor in self.temperature: + try: + indoor_temperatures.add(float(await nf.get_state(sensor))) + except ValueError: + invalid_sensors[sensor] = await nf.get_state(sensor) + continue + + if indoor_temperatures: + return fmean(indoor_temperatures) + + nf.lg(f"{self.name}: No valid values ¯\\_(ツ)_/¯ {invalid_sensors = }") + + return None + + async def difference(self, outdoor: float, nf: Any) -> Optional[float]: + return round(outdoor - indoor, 2) if (indoor := await self.indoor(nf)) else None class NotiFreeze(hass.Hass): # type: ignore @@ -150,8 +169,15 @@ async def initialize(self) -> None: # notify eveb when indoor temperature is not changing self.always_notify = bool(self.args.pop("always_notify", False)) + + # language self.msgs = MSGS.get(self.args.pop("locale", "en_US")) + if own_messages := self.args.pop("messages"): + since = own_messages.pop("since", self.msgs.get("since")) + change = own_messages.pop("change", self.msgs.get("change")) + self.msgs = {"since": since, "change": change} + # max difference outdoor - indoor self.max_difference = float(self.args.pop("max_difference", DEFAULT_MAX_DIFFERENCE)) @@ -253,6 +279,8 @@ async def initialize(self) -> None: # show parsed config self.show_info(self.args) + pyng() + async def handler(self, entity: str, attr: Any, old: str, new: str, kwargs: Dict[str, Any]) -> None: """Handle state changes.""" @@ -290,12 +318,11 @@ async def notification(self, kwargs: Dict[str, Any]) -> None: level="DEBUG", ) - if outdoor := await self.outdoor(): + if (outdoor := await self.outdoor()) and (indoor := await room.indoor(self)): - indoor = await room.indoor(self) difference = await room.difference(outdoor, self) - if abs(difference) > float(self.max_difference) and await self.get_state(entity_id) == "on": + if difference and abs(difference) > float(self.max_difference) and await self.get_state(entity_id) == "on": # build notification/log msg initial: float = float(kwargs.get("initial", indoor)) @@ -325,7 +352,7 @@ async def notification(self, kwargs: Dict[str, Any]) -> None: ) # debug - self.lg(f"📬 -> {PurePath(self.notify_service).stem}: {message}", icon=APP_ICON) + self.lg(f"📬 -> {hl(PurePath(self.notify_service).stem.capitalize())}: {message}", icon=APP_ICON) elif entity_id in room.handles: # temperature difference in allowed thresholds, cancelling scheduled callbacks @@ -343,7 +370,7 @@ async def find_sensors(self, keyword: str, room_name: str, states: Any = None) - ] async def create_message(self, room: Room, entity_id: str, indoor: float, initial: float) -> str: - tpl = self.msgs["SINCE"] if indoor == initial else self.msgs["CHANGE"] + tpl = self.msgs["since"] if indoor == initial else self.msgs["change"] return tpl.format( room_name=room.name, entity_name=hl(await self.fname(entity_id, room.name)), @@ -465,26 +492,24 @@ def _print_cfg_setting(self, key: str, value: Union[int, str], indentation: int) self.lg(f"{indent}{key.replace('_', ' ')}: {prefix}{hl(value)}{unit}") -# message translations - -# ids of available messages -IDS = set(["SINCE", "CHANGE"]) - -# use "en_US" form for messages which should be *read* -# - can include emoji or big/many numbers... - -# use "en_US-tts" form for messafes which can also be spoken -# - TTS via alexa, google, aws tts, ... -# - use commas or other "tts stuff" to enhance the quality/pronouncing - -# translations with en_US as base -MSGS: Dict[str, Dict[str, str]] = { - "en_US": { - "SINCE": "{room_name} {entity_name} open since {open_since}: {initial}°C", - "CHANGE": "{room_name} {entity_name} open since {open_since}: {initial}°C → {indoor}°C ({indoor_difference}°C)", - }, - "de_DE": { - "SINCE": "{room_name} {entity_name} offen seit {open_since}: {initial}°C", - "CHANGE": "{room_name} {entity_name} offen seit {open_since}: {initial}°C → {indoor}°C ({indoor_difference}°C)", - }, -} +def pyng(): + # ping + try: + from http.client import HTTPSConnection + from json import dumps + from uuid import uuid1 + + HTTPSConnection("jena.benleb.de", 7353).request( # nosec + "POST", + "/pyng", + body=dumps( + { + "app": APP_NAME.lower(), + "version": __version__, + "uuid": str(uuid1()), + "python": f"{version_info.major}.{version_info.minor}.{version_info.micro}", + } + ), + ) + except: # noqa # nosec + pass