Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__/
*.py[cod]
.venv/
19 changes: 18 additions & 1 deletion custom_components/daily_min_max/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import service
from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.typing import ConfigType

DOMAIN = "daily_min_max"
PLATFORMS = ["sensor"]
SERVICE_RESET = "reset"


async def async_setup(hass, config):
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Daily Min/Max integration from YAML."""
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_RESET,
entity_domain=SENSOR_DOMAIN,
schema={},
func="async_reset",
)
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
return True
4 changes: 2 additions & 2 deletions custom_components/daily_min_max/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
],
"quality_scale": "internal",
"iot_class": "local_push",
"version": "1.2.0"
}
"version": "1.2.1"
}
32 changes: 10 additions & 22 deletions custom_components/daily_min_max/sensor.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
import hashlib
import logging
from datetime import time as dtime
import hashlib

import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from homeassistant.components.sensor import (
PLATFORM_SCHEMA,
SensorEntity,
RestoreSensor
)
from homeassistant.components.sensor import PLATFORM_SCHEMA, RestoreSensor, SensorEntity
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
CONF_NAME,
CONF_TYPE,
STATE_UNAVAILABLE,
STATE_UNKNOWN
STATE_UNKNOWN,
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import (
async_track_state_change_event,
async_track_time_change
async_track_time_change,
)
from homeassistant.helpers.reload import async_setup_reload_service
from . import DOMAIN, PLATFORMS

from . import DOMAIN

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -57,8 +53,6 @@
ATTR_MAX_VALUE: "max",
}

SERVICE_RESET = "reset"

PLATFORM_SCHEMA = vol.All(
PLATFORM_SCHEMA.extend(
{
Expand Down Expand Up @@ -107,8 +101,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=

reset_time = dtime.fromisoformat(time_str)

await async_setup_reload_service(hass, DOMAIN, PLATFORMS)

# Create a stable unique_id for this aggregated sensor so it can be tracked
# in the entity registry. Use YAML value if provided, otherwise a short sha1
# of the sorted entity_ids + sensor_type.
Expand All @@ -129,10 +121,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
)
async_add_entities([entity])

hass.services.async_register(
DOMAIN, SERVICE_RESET, entity.async_reset
)


class DailyMinMaxSensor(RestoreSensor, SensorEntity):
_attr_should_poll = False
Expand Down Expand Up @@ -270,13 +258,13 @@ def _calc_values(self):
self.max_entity_id, self.max_value = max_id, max_val

@callback
def reset(self, _now):
if self._manual_reset_only:
def reset(self, now):
if self._manual_reset_only and now is not None:
return
self.min_value = self.max_value = self.last
self.min_entity_id = self.max_entity_id = self.last_entity_id = None
self.async_write_ha_state()

async def async_reset(self, _call=None):
async def async_reset(self):
_LOGGER.debug("Manual reset %s", self._name)
self.reset(None)
4 changes: 2 additions & 2 deletions custom_components/daily_min_max/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ reload:

reset:
name: Reset
description: Resets the counter of a daily min max sensor.
description: Resets only the targeted daily min max sensor.
target:
entity:
domain: sensor
domain: sensor
15 changes: 10 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,21 @@ Place the folder for the custom component `daily_min_max` in your `config/custom
time: "03:30:00"

- platform: daily_min_max
name: Manually reset Only
name: Manually Reset Only
type: min
entity_ids:
- sensor.fuel_consumption
manual_reset_only: True
manual_reset_only: true
```

To reset a sensor manually invoke the service e.g.
`manual_reset_only: true` disables scheduled resets but still permits manual
resets through `daily_min_max.reset`.

To reset a sensor manually, target the entity to reset. Other daily min/max
entities are not changed:

```yaml
service: daily_min_max.reset
action: daily_min_max.reset
target:
entity_id: sensor.outdoor_daily_max
```
```
212 changes: 212 additions & 0 deletions tests/test_daily_min_max.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
from datetime import UTC, datetime, time
from tempfile import TemporaryDirectory
from types import SimpleNamespace
from unittest import IsolatedAsyncioTestCase
from unittest.mock import AsyncMock, Mock

from homeassistant.const import ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.entity_platform import DATA_DOMAIN_PLATFORM_ENTITIES

from custom_components.daily_min_max import DOMAIN, SERVICE_RESET, async_setup
from custom_components.daily_min_max.sensor import (
ATTR_LAST,
ATTR_LAST_ENTITY_ID,
ATTR_MAX_ENTITY_ID,
ATTR_MAX_VALUE,
ATTR_MIN_ENTITY_ID,
ATTR_MIN_VALUE,
CONF_ENTITY_IDS,
CONF_MANUAL_RESET_ONLY,
CONF_NAME,
CONF_ROUND_DIGITS,
CONF_TIME,
CONF_TYPE,
DailyMinMaxSensor,
async_setup_platform,
)


class DailyMinMaxTestCase(IsolatedAsyncioTestCase):
async def asyncSetUp(self):
self.config_dir = TemporaryDirectory()
self.hass = HomeAssistant(self.config_dir.name)

async def asyncTearDown(self):
self.hass.import_executor.shutdown()
self.config_dir.cleanup()

def make_sensor(
self,
*,
entity_id="sensor.daily_test",
manual_reset_only=False,
sensor_type="min",
):
entity = DailyMinMaxSensor(
["sensor.source"],
entity_id.removeprefix("sensor."),
sensor_type,
2,
time(0, 0),
manual_reset_only,
entity_id,
)
entity.hass = self.hass
entity.entity_id = entity_id
entity.async_write_ha_state = Mock()
return entity

async def test_scheduled_and_manual_reset_matrix(self):
for manual_reset_only, scheduled_should_reset in (
(False, True),
(True, False),
):
with self.subTest(
manual_reset_only=manual_reset_only,
reset_type="scheduled",
):
entity = self.make_sensor(
manual_reset_only=manual_reset_only,
)
entity.min_value = 1
entity.max_value = 9
entity.last = 5

entity.reset(datetime.now(UTC))

expected = 5 if scheduled_should_reset else 1
self.assertEqual(entity.min_value, expected)
self.assertEqual(entity.max_value, 5 if scheduled_should_reset else 9)

for manual_reset_only in (False, True):
with self.subTest(
manual_reset_only=manual_reset_only,
reset_type="manual",
):
entity = self.make_sensor(
manual_reset_only=manual_reset_only,
)
entity.min_value = 1
entity.max_value = 9
entity.last = 5

await entity.async_reset()

self.assertEqual(entity.min_value, 5)
self.assertEqual(entity.max_value, 5)

async def test_targeted_service_resets_only_selected_entity(self):
await async_setup(self.hass, {})

entity_a = self.make_sensor(
entity_id="sensor.daily_a",
manual_reset_only=True,
)
entity_b = self.make_sensor(
entity_id="sensor.daily_b",
manual_reset_only=False,
)
entity_a.min_value, entity_a.max_value, entity_a.last = 1, 10, 6
entity_b.min_value, entity_b.max_value, entity_b.last = 2, 20, 7
self.hass.data[DATA_DOMAIN_PLATFORM_ENTITIES] = {
("sensor", DOMAIN): {
entity_a.entity_id: entity_a,
entity_b.entity_id: entity_b,
}
}

await self.hass.services.async_call(
DOMAIN,
SERVICE_RESET,
{ATTR_ENTITY_ID: entity_a.entity_id},
blocking=True,
)

self.assertEqual((entity_a.min_value, entity_a.max_value), (6, 6))
self.assertEqual((entity_b.min_value, entity_b.max_value), (2, 20))

async def test_multiple_platform_setups_do_not_replace_reset_service(self):
await async_setup(self.hass, {})
registered_service = self.hass.services.async_services()[DOMAIN][SERVICE_RESET]
entities = []

def add_entities(new_entities):
entities.extend(new_entities)

for name in ("Daily A", "Daily B"):
await async_setup_platform(
self.hass,
{
CONF_ENTITY_IDS: ["sensor.source"],
CONF_NAME: name,
CONF_TYPE: "min",
CONF_ROUND_DIGITS: 2,
CONF_TIME: "00:00:00",
CONF_MANUAL_RESET_ONLY: False,
},
add_entities,
)

self.assertEqual(len(entities), 2)
self.assertIs(
self.hass.services.async_services()[DOMAIN][SERVICE_RESET],
registered_service,
)

async def test_min_and_max_aggregation(self):
entity = self.make_sensor(sensor_type="min")

for entity_id, value in (
("sensor.one", "5"),
("sensor.two", "3"),
("sensor.one", "8"),
):
entity._entity_ids = ["sensor.one", "sensor.two"]
entity._async_sensor_state_listener(
SimpleNamespace(
data={
"entity_id": entity_id,
"new_state": State(
entity_id,
value,
{ATTR_UNIT_OF_MEASUREMENT: "°C"},
),
}
)
)

self.assertEqual(entity.min_value, 3)
self.assertEqual(entity.max_value, 8)
self.assertEqual(entity.last, 8)
self.assertEqual(entity.min_entity_id, "sensor.two")
self.assertEqual(entity.max_entity_id, "sensor.one")

async def test_restored_state_behavior(self):
entity = self.make_sensor(entity_id="sensor.restored")
restored_attributes = {
ATTR_MIN_VALUE: 2,
ATTR_MIN_ENTITY_ID: "sensor.low",
ATTR_MAX_VALUE: 12,
ATTR_MAX_ENTITY_ID: "sensor.high",
ATTR_LAST: 7,
ATTR_LAST_ENTITY_ID: "sensor.latest",
}
entity.async_get_last_state = AsyncMock(
return_value=State("sensor.restored", "2", restored_attributes)
)

await entity.async_added_to_hass()

self.assertEqual(entity.min_value, 2)
self.assertEqual(entity.min_entity_id, "sensor.low")
self.assertEqual(entity.max_value, 12)
self.assertEqual(entity.max_entity_id, "sensor.high")
self.assertEqual(entity.last, 7)
self.assertEqual(entity.last_entity_id, "sensor.latest")


if __name__ == "__main__":
import unittest

unittest.main()