Skip to content

Commit

Permalink
adjust start time if duration changes within interval
Browse files Browse the repository at this point in the history
  • Loading branch information
mampfes committed Mar 10, 2024
1 parent 6b76508 commit 14563f6
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 40 deletions.
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,52 @@ In case you would like to install manually:
[![Open your Home Assistant instance and start setting up a new helpers.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start?domain=epex_spot_sensor)

If the button doesn't work: Open `Settings` > `Devices & services` > `Helpers` > `Create Helper` and select `EPEX Spot Sensor`.

## Configuration Options

1. Earliest Start Time
Earliest time to start the appliance.

2. Latest End Time
Latest time to end the appliance. Set it to same value as earliest start time to cover 24h. If set to smaller value than earliest start time, it automatically refers to following day.

3. Duration
Required duration to complete the appliance.

4. Remaining Duration Entity
Optional entity which indicates the remaining duration. If entity is set, it replaces the static duration. If the state of the `Remaining Duration Entity` changes between `Earliest Start Time` and `Latest End Time`, the configured `Earliest Start Time` will be ignore and the latest change time of the `Remaining Duration Entity` will the used instead.

5. Price Mode
Selects whether the sensor shall react on the cheapest or the most expensive prices between `Earliest Start Time` and `Latest End Time`.

6. Interval Mode
Selects whether the specified duration shall be completed in a single, contiguous interval or can be split into multiple, not contiguous intervals (`intermittend`).

## Sensor Attributes

1. Earliest Start Time
Reflects the configured `Earliest Start Time`.

2. Latest End Time
Reflects the configured `Latest End Time`.

3. Duration
Reflects the used value for duration, which is either the configured duration if the `Remaining Duration Entity` is not set; or the state of the `Remaining Duration Entity`.

4. Remaining Duration Entity
Optional entity which indicates the remaining duration. If entity is set, it replaces the static duration.

5. Interval Start Time
Reflects the actual start time of the interval, which is either the configured `Earliest Start Time` or the latest change time of the `Remaining Duration Entity` if the state of the entity changed between `Earliest Start Time` and `Latest End Time`.

6. Price Mode
Reflects the configured `Price Mode`.

7. Price Mode
Reflects the configured `Interval Mode`.

8. Enabled
Set to `true` if current time is between `Earliest Start Time` and `Latest End Time`.

9. Data
List of calculated intervals to switch sensor on, consisting of `start_time`, `end_time` and `rank` (for Interval Mode intermittend only).
81 changes: 48 additions & 33 deletions custom_components/epex_spot_sensor/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
CONF_LATEST_END_TIME,
CONF_DURATION,
CONF_DURATION_ENTITY_ID,
CONF_INTERVAL_START_TIME,
CONF_PRICE_MODE,
CONF_INTERVAL_MODE,
)
Expand Down Expand Up @@ -165,53 +166,33 @@ def __init__(

# calculated values
self._duration: timedelta = self._default_duration
self._interval_start_time = None
self._interval_enabled: bool = False
self._state: bool | None = None
self._intervals: list | None = None

def _on_price_sensor_state_update() -> None:
"""Handle sensor state changes."""

# set to unavailable by default
self._sensor_attributes = None
self._state = None

if (new_state := hass.states.get(self._entity_id)) is None:
# _LOGGER.warning(f"Can't get states of {self._entity_id}")
return

try:
self._sensor_attributes = new_state.attributes
except (ValueError, TypeError):
_LOGGER.warning(f"Can't get attributes of {self._entity_id}")
return

self._calculate_duration()

self._update_state()

@callback
def async_price_sensor_state_listener(
def async_update_state(
event: EventType[EventStateChangedData],
) -> None:
"""Handle sensor state changes."""
_on_price_sensor_state_update()
self.async_write_ha_state()
"""Handle price or duration sensor state changes."""
self._update_state()

entities_to_track = [entity_id]
if duration_entity_id is not None:
entities_to_track.append(duration_entity_id)
self.async_on_remove(
async_track_state_change_event(
hass, entities_to_track, async_price_sensor_state_listener
)
async_track_state_change_event(hass, entities_to_track, async_update_state)
)

# check every minute for new states
self.async_on_remove(
async_track_time_change(hass, async_price_sensor_state_listener, second=0)
async_track_time_change(hass, async_update_state, second=0)
)
_on_price_sensor_state_update()

async def async_added_to_hass(self) -> None:
"""manually trigger first update"""
self._update_state()

@property
def is_available(self) -> bool | None:
Expand All @@ -231,6 +212,7 @@ def extra_state_attributes(self) -> dict[str, Any]:
CONF_EARLIEST_START_TIME: self._earliest_start_time,
CONF_LATEST_END_TIME: self._latest_end_time,
CONF_DURATION: str(self._duration),
CONF_INTERVAL_START_TIME: self._interval_start_time,
CONF_PRICE_MODE: self._price_mode,
CONF_INTERVAL_MODE: self._interval_mode,
ATTR_INTERVAL_ENABLED: self._interval_enabled,
Expand All @@ -239,6 +221,21 @@ def extra_state_attributes(self) -> dict[str, Any]:

@callback
def _update_state(self) -> None:
# set to unavailable by default
self._sensor_attributes = None
self._state = None

# get price sensor attributes first
if (new_state := self._hass.states.get(self._entity_id)) is None:
# _LOGGER.warning(f"Can't get states of {self._entity_id}")
return

try:
self._sensor_attributes = new_state.attributes
except (ValueError, TypeError):
_LOGGER.warning(f"Can't get attributes of {self._entity_id}")
return

now = dt_util.now()

# earliest_start always refers to today
Expand All @@ -262,14 +259,24 @@ def _update_state(self) -> None:
latest_end += timedelta(days=1)

self._interval_enabled = earliest_start <= now <= latest_end
self._interval_start_time = earliest_start

# calculate the actual duration (in case a duration entity is configured)
self._calculate_duration()

if self._interval_mode == IntervalModes.INTERMITTENT.value:
self._update_state_for_intermittent(earliest_start, latest_end, now)
self._update_state_for_intermittent(
self._interval_start_time, latest_end, now
)
elif self._interval_mode == IntervalModes.CONTIGUOUS.value:
self._update_state_for_contigous(earliest_start, latest_end, now)
self._update_state_for_contiguous(
self._interval_start_time, latest_end, now
)
else:
_LOGGER.error(f"invalid interval mode: {self._interval_mode}")

self.async_write_ha_state()

def _update_state_for_intermittent(
self, earliest_start: time, latest_end: time, now: datetime
):
Expand Down Expand Up @@ -319,7 +326,7 @@ def _update_state_for_intermittent(
for e in sorted(intervals, key=lambda e: e.start_time)
]

def _update_state_for_contigous(
def _update_state_for_contiguous(
self, earliest_start: time, latest_end: time, now: datetime
):
marketdata = self._get_marketdata()
Expand Down Expand Up @@ -421,3 +428,11 @@ def _calculate_duration(self):
self._duration = cv.time_period_dict(
{DURATION_UOM_MAP[uom]: float(duration_entity_state.state)}
)

# set interval start time to duration entity last_changed
# if duration entity is changed within interval
if (
self._interval_enabled
and duration_entity_state.last_changed > self._interval_start_time
):
self._interval_start_time = duration_entity_state.last_changed
1 change: 1 addition & 0 deletions custom_components/epex_spot_sensor/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
CONF_LATEST_END_TIME = "latest_end_time"
CONF_DURATION = "duration"
CONF_DURATION_ENTITY_ID = "duration_entity_id"
CONF_INTERVAL_START_TIME = "interval_start_time"

CONF_INTERVAL_MODE = "interval_mode"

Expand Down
2 changes: 1 addition & 1 deletion custom_components/epex_spot_sensor/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"integration_type": "helper",
"iot_class": "calculated",
"issue_tracker": "https://github.com/mampfes/ha_epex_spot_sensor/issues",
"version": "1.1.0"
"version": "1.2.0"
}
12 changes: 6 additions & 6 deletions custom_components/epex_spot_sensor/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@
"earliest_start_time": "Earliest Start Time",
"latest_end_time": "Latest End Time",
"duration": "Duration",
"duration_entity_id": "Duration Entity",
"duration_entity_id": "Remaining Duration Entity",
"interval_mode": "Interval Mode",
"price_mode": "Price Mode"
},
"data_description": {
"earliest_start_time": "Earliest time to start the appliance.",
"latest_end_time": "Latest time to end the appliance. Set it to same value as earliest start time to cover 24h. If set to smaller value than earliest start time, it automatically refers to following day.",
"duration": "Required duration to complete the appliance.",
"duration_entity_id": "Optional entity which holds duration. If entity is set, it replaces the static duration.",
"interval_mode": "Does the appliance need a single contigous interval or can it be splitted into multiple intervals."
"duration_entity_id": "Optional entity which indicates the remaining duration. If entity is set, it replaces the static duration.",
"interval_mode": "Does the appliance need a single contiguous interval or can it be splitted into multiple intervals."
},
"description": "Create a binary sensor that turns on or off depending on the market price.",
"title": "Add EPEX Spot Binary Sensor"
Expand All @@ -31,16 +31,16 @@
"earliest_start_time": "Earliest Start Time",
"latest_end_time": "Latest End Time",
"duration": "Duration",
"duration_entity_id": "Duration Entity",
"duration_entity_id": "Remaining Duration Entity",
"interval_mode": "Interval Mode",
"price_mode": "Price Mode"
},
"data_description": {
"earliest_start_time": "Earliest time to start the appliance.",
"latest_end_time": "Latest time to end the appliance. Set it to same value as earliest start time to cover 24h. If set to smaller value than earliest start time, it automatically refers to following day.",
"duration": "Required duration to complete the appliance.",
"duration_entity_id": "Optional entity which holds duration. If entity is set, it replaces the static duration.",
"interval_mode": "Does the appliance need a single contigous interval or can it be splitted into multiple intervals."
"duration_entity_id": "Optional entity which indicates the remaining duration. If entity is set, it replaces the static duration.",
"interval_mode": "Does the appliance need a single contiguous interval or can it be splitted into multiple intervals."
}
}
}
Expand Down

0 comments on commit 14563f6

Please sign in to comment.