mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-07-28 17:46:49 +00:00
Compare commits
5 commits
6937068a7a
...
e39d0be162
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e39d0be162 | ||
|
|
bcbaf3a1cd | ||
|
|
3a44bc8267 | ||
|
|
5ea6e75295 | ||
|
|
db5d172fb8 |
13 changed files with 151 additions and 41 deletions
|
|
@ -1460,7 +1460,7 @@ plan_charging:
|
||||||
number:
|
number:
|
||||||
min: 0
|
min: 0
|
||||||
max: 100000
|
max: 100000
|
||||||
step: 0.0001
|
step: any
|
||||||
mode: box
|
mode: box
|
||||||
reserve_for_discharge:
|
reserve_for_discharge:
|
||||||
required: false
|
required: false
|
||||||
|
|
@ -1474,7 +1474,7 @@ plan_charging:
|
||||||
number:
|
number:
|
||||||
min: 0
|
min: 0
|
||||||
max: 100000
|
max: 100000
|
||||||
step: 0.0001
|
step: any
|
||||||
mode: box
|
mode: box
|
||||||
output:
|
output:
|
||||||
collapsed: true
|
collapsed: true
|
||||||
|
|
|
||||||
|
|
@ -267,8 +267,11 @@ async def _handle_find_block(
|
||||||
# Round up to nearest quarter-hour interval
|
# Round up to nearest quarter-hour interval
|
||||||
duration_minutes = math.ceil(duration_minutes_requested / INTERVAL_MINUTES) * INTERVAL_MINUTES
|
duration_minutes = math.ceil(duration_minutes_requested / INTERVAL_MINUTES) * INTERVAL_MINUTES
|
||||||
|
|
||||||
entry, coordinator, data = get_entry_and_data(hass, entry_id)
|
# Note: rebind to coordinator_data — `data` (the resolved service call
|
||||||
rating_lookup = build_rating_lookup(data)
|
# data) is still needed below for validate_search_params() and
|
||||||
|
# apply_must_finish_by(), which read search-range parameters from it.
|
||||||
|
entry, coordinator, coordinator_data = get_entry_and_data(hass, entry_id)
|
||||||
|
rating_lookup = build_rating_lookup(coordinator_data)
|
||||||
|
|
||||||
home_id = entry.data.get("home_id")
|
home_id = entry.data.get("home_id")
|
||||||
if not home_id:
|
if not home_id:
|
||||||
|
|
|
||||||
|
|
@ -329,8 +329,11 @@ async def _handle_find_hours(
|
||||||
total_minutes = math.ceil(total_minutes_requested / INTERVAL_MINUTES) * INTERVAL_MINUTES
|
total_minutes = math.ceil(total_minutes_requested / INTERVAL_MINUTES) * INTERVAL_MINUTES
|
||||||
min_segment_minutes = math.ceil(min_segment_minutes_requested / INTERVAL_MINUTES) * INTERVAL_MINUTES
|
min_segment_minutes = math.ceil(min_segment_minutes_requested / INTERVAL_MINUTES) * INTERVAL_MINUTES
|
||||||
|
|
||||||
entry, coordinator, data = get_entry_and_data(hass, entry_id)
|
# Note: rebind to coordinator_data — `data` (the resolved service call
|
||||||
rating_lookup = build_rating_lookup(data)
|
# data) is still needed below for validate_search_params() and
|
||||||
|
# apply_must_finish_by(), which read search-range parameters from it.
|
||||||
|
entry, coordinator, coordinator_data = get_entry_and_data(hass, entry_id)
|
||||||
|
rating_lookup = build_rating_lookup(coordinator_data)
|
||||||
|
|
||||||
home_id = entry.data.get("home_id")
|
home_id = entry.data.get("home_id")
|
||||||
if not home_id:
|
if not home_id:
|
||||||
|
|
|
||||||
|
|
@ -406,8 +406,8 @@ async def handle_find_cheapest_schedule(call: ServiceCall) -> ServiceResponse:
|
||||||
# Round gap up to nearest quarter interval
|
# Round gap up to nearest quarter interval
|
||||||
gap_intervals = math.ceil(gap_minutes / INTERVAL_MINUTES) if gap_minutes > 0 else 0
|
gap_intervals = math.ceil(gap_minutes / INTERVAL_MINUTES) if gap_minutes > 0 else 0
|
||||||
|
|
||||||
entry, coordinator, data = get_entry_and_data(hass, entry_id)
|
entry, coordinator, coordinator_data = get_entry_and_data(hass, entry_id)
|
||||||
rating_lookup = build_rating_lookup(data)
|
rating_lookup = build_rating_lookup(coordinator_data)
|
||||||
|
|
||||||
home_id = entry.data.get("home_id")
|
home_id = entry.data.get("home_id")
|
||||||
if not home_id:
|
if not home_id:
|
||||||
|
|
|
||||||
|
|
@ -1271,7 +1271,7 @@
|
||||||
"message": "array_fields kann nur mit output_format: array_of_arrays verwendet werden. Ändere das Ausgabeformat oder entferne array_fields."
|
"message": "array_fields kann nur mit output_format: array_of_arrays verwendet werden. Ändere das Ausgabeformat oder entferne array_fields."
|
||||||
},
|
},
|
||||||
"invalid_array_fields": {
|
"invalid_array_fields": {
|
||||||
"message": "Der Wert {template} für array_fields ist ungültig. Feldnamen müssen in geschweifte Klammern eingeschlossen sein, z.B. '{start_time}, {price_per_kwh}, {level}'."
|
"message": "Der Wert {template} für array_fields ist ungültig. Feldnamen müssen in geschweifte Klammern eingeschlossen sein, z.B. {start_time}, {price_per_kwh}, {level}."
|
||||||
},
|
},
|
||||||
"min_segment_exceeds_duration": {
|
"min_segment_exceeds_duration": {
|
||||||
"message": "min_segment_duration ({min_segment_minutes} Min.) darf die Gesamtdauer ({duration_minutes} Min.) nicht überschreiten. Reduziere min_segment_duration oder erhöhe duration."
|
"message": "min_segment_duration ({min_segment_minutes} Min.) darf die Gesamtdauer ({duration_minutes} Min.) nicht überschreiten. Reduziere min_segment_duration oder erhöhe duration."
|
||||||
|
|
@ -1298,19 +1298,19 @@
|
||||||
"message": "must_finish_by kann nicht mit Endgrenz-Parametern ({params}) kombiniert werden. Verwende must_finish_by allein — es setzt das Suchende automatisch auf die Deadline."
|
"message": "must_finish_by kann nicht mit Endgrenz-Parametern ({params}) kombiniert werden. Verwende must_finish_by allein — es setzt das Suchende automatisch auf die Deadline."
|
||||||
},
|
},
|
||||||
"invalid_entity_reference": {
|
"invalid_entity_reference": {
|
||||||
"message": "'{reference}' ist keine gültige Entity-Referenz. Verwende das Format 'domain.entity_id' oder 'domain.entity_id@attribut'."
|
"message": "{reference} ist keine gültige Entity-Referenz. Verwende das Format 'domain.entity_id' oder 'domain.entity_id@attribut'."
|
||||||
},
|
},
|
||||||
"entity_not_found": {
|
"entity_not_found": {
|
||||||
"message": "Entity '{entity_id}' nicht gefunden. Überprüfe, ob die Entity existiert und verfügbar ist."
|
"message": "Entity {entity_id} nicht gefunden. Überprüfe, ob die Entity existiert und verfügbar ist."
|
||||||
},
|
},
|
||||||
"entity_attribute_not_found": {
|
"entity_attribute_not_found": {
|
||||||
"message": "Entity '{entity_id}' hat kein Attribut '{attribute}'."
|
"message": "Entity {entity_id} hat kein Attribut {attribute}."
|
||||||
},
|
},
|
||||||
"entity_state_unavailable": {
|
"entity_state_unavailable": {
|
||||||
"message": "Entity '{entity_id}' hat den Status '{state}'. Die Entity muss einen gültigen Statuswert haben."
|
"message": "Entity {entity_id} hat den Status {state}. Die Entity muss einen gültigen Statuswert haben."
|
||||||
},
|
},
|
||||||
"entity_value_conversion_failed": {
|
"entity_value_conversion_failed": {
|
||||||
"message": "Wert '{raw_value}' von '{entity_id}' ({attribute}) kann nicht in {expected_type} konvertiert werden. Überprüfe, ob die Entity einen kompatiblen Wert liefert."
|
"message": "Wert {raw_value} von {entity_id} ({attribute}) kann nicht in {expected_type} konvertiert werden. Überprüfe, ob die Entity einen kompatiblen Wert liefert."
|
||||||
},
|
},
|
||||||
"capacity_required_for_percent": {
|
"capacity_required_for_percent": {
|
||||||
"message": "battery_capacity_kwh ist erforderlich, wenn der aktuelle oder Ziel-SoC in Prozent angegeben wird."
|
"message": "battery_capacity_kwh ist erforderlich, wenn der aktuelle oder Ziel-SoC in Prozent angegeben wird."
|
||||||
|
|
|
||||||
|
|
@ -1271,7 +1271,7 @@
|
||||||
"message": "array_fields can only be used with output_format: array_of_arrays. Change the output format or remove array_fields."
|
"message": "array_fields can only be used with output_format: array_of_arrays. Change the output format or remove array_fields."
|
||||||
},
|
},
|
||||||
"invalid_array_fields": {
|
"invalid_array_fields": {
|
||||||
"message": "The array_fields value {template} is invalid. Field names must be wrapped in curly braces, e.g. '{start_time}, {price_per_kwh}, {level}'."
|
"message": "The array_fields value {template} is invalid. Field names must be wrapped in curly braces, e.g. {start_time}, {price_per_kwh}, {level}."
|
||||||
},
|
},
|
||||||
"min_segment_exceeds_duration": {
|
"min_segment_exceeds_duration": {
|
||||||
"message": "min_segment_duration ({min_segment_minutes} min) cannot exceed the total duration ({duration_minutes} min). Reduce min_segment_duration or increase duration."
|
"message": "min_segment_duration ({min_segment_minutes} min) cannot exceed the total duration ({duration_minutes} min). Reduce min_segment_duration or increase duration."
|
||||||
|
|
@ -1298,19 +1298,19 @@
|
||||||
"message": "must_finish_by cannot be combined with end-boundary parameters ({params}). Use must_finish_by alone — it sets the search end to the deadline automatically."
|
"message": "must_finish_by cannot be combined with end-boundary parameters ({params}). Use must_finish_by alone — it sets the search end to the deadline automatically."
|
||||||
},
|
},
|
||||||
"invalid_entity_reference": {
|
"invalid_entity_reference": {
|
||||||
"message": "'{reference}' is not a valid entity reference. Use the format 'domain.entity_id' or 'domain.entity_id@attribute'."
|
"message": "{reference} is not a valid entity reference. Use the format 'domain.entity_id' or 'domain.entity_id@attribute'."
|
||||||
},
|
},
|
||||||
"entity_not_found": {
|
"entity_not_found": {
|
||||||
"message": "Entity '{entity_id}' not found. Verify the entity exists and is available."
|
"message": "Entity {entity_id} not found. Verify the entity exists and is available."
|
||||||
},
|
},
|
||||||
"entity_attribute_not_found": {
|
"entity_attribute_not_found": {
|
||||||
"message": "Entity '{entity_id}' does not have attribute '{attribute}'."
|
"message": "Entity {entity_id} does not have attribute {attribute}."
|
||||||
},
|
},
|
||||||
"entity_state_unavailable": {
|
"entity_state_unavailable": {
|
||||||
"message": "Entity '{entity_id}' state is '{state}'. The entity must have a valid state value."
|
"message": "Entity {entity_id} state is {state}. The entity must have a valid state value."
|
||||||
},
|
},
|
||||||
"entity_value_conversion_failed": {
|
"entity_value_conversion_failed": {
|
||||||
"message": "Cannot convert value '{raw_value}' from '{entity_id}' ({attribute}) to {expected_type}. Verify the entity provides a compatible value."
|
"message": "Cannot convert value {raw_value} from {entity_id} ({attribute}) to {expected_type}. Verify the entity provides a compatible value."
|
||||||
},
|
},
|
||||||
"capacity_required_for_percent": {
|
"capacity_required_for_percent": {
|
||||||
"message": "battery_capacity_kwh is required when current or target SoC is provided as a percentage."
|
"message": "battery_capacity_kwh is required when current or target SoC is provided as a percentage."
|
||||||
|
|
|
||||||
|
|
@ -1271,7 +1271,7 @@
|
||||||
"message": "array_fields kan kun brukes med output_format: array_of_arrays. Endre utdataformatet eller fjern array_fields."
|
"message": "array_fields kan kun brukes med output_format: array_of_arrays. Endre utdataformatet eller fjern array_fields."
|
||||||
},
|
},
|
||||||
"invalid_array_fields": {
|
"invalid_array_fields": {
|
||||||
"message": "Verdien {template} for array_fields er ugyldig. Feltnavn må omsluttes av krøllparenteser, f.eks. '{start_time}, {price_per_kwh}, {level}'."
|
"message": "Verdien {template} for array_fields er ugyldig. Feltnavn må omsluttes av krøllparenteser, f.eks. {start_time}, {price_per_kwh}, {level}."
|
||||||
},
|
},
|
||||||
"min_segment_exceeds_duration": {
|
"min_segment_exceeds_duration": {
|
||||||
"message": "min_segment_duration ({min_segment_minutes} min) kan ikke overstige total varighet ({duration_minutes} min). Reduser min_segment_duration eller øk duration."
|
"message": "min_segment_duration ({min_segment_minutes} min) kan ikke overstige total varighet ({duration_minutes} min). Reduser min_segment_duration eller øk duration."
|
||||||
|
|
@ -1298,19 +1298,19 @@
|
||||||
"message": "must_finish_by kan ikke kombineres med sluttgrenseparametere ({params}). Bruk must_finish_by alene — det setter søkeslutt til fristen automatisk."
|
"message": "must_finish_by kan ikke kombineres med sluttgrenseparametere ({params}). Bruk must_finish_by alene — det setter søkeslutt til fristen automatisk."
|
||||||
},
|
},
|
||||||
"invalid_entity_reference": {
|
"invalid_entity_reference": {
|
||||||
"message": "'{reference}' er ikke en gyldig entitetsreferanse. Bruk formatet 'domain.entity_id' eller 'domain.entity_id@attributt'."
|
"message": "{reference} er ikke en gyldig entitetsreferanse. Bruk formatet 'domain.entity_id' eller 'domain.entity_id@attributt'."
|
||||||
},
|
},
|
||||||
"entity_not_found": {
|
"entity_not_found": {
|
||||||
"message": "Entitet '{entity_id}' ble ikke funnet. Kontroller at entiteten finnes og er tilgjengelig."
|
"message": "Entitet {entity_id} ble ikke funnet. Kontroller at entiteten finnes og er tilgjengelig."
|
||||||
},
|
},
|
||||||
"entity_attribute_not_found": {
|
"entity_attribute_not_found": {
|
||||||
"message": "Entitet '{entity_id}' har ikke attributtet '{attribute}'."
|
"message": "Entitet {entity_id} har ikke attributtet {attribute}."
|
||||||
},
|
},
|
||||||
"entity_state_unavailable": {
|
"entity_state_unavailable": {
|
||||||
"message": "Entitet '{entity_id}' har tilstanden '{state}'. Entiteten må ha en gyldig tilstandsverdi."
|
"message": "Entitet {entity_id} har tilstanden {state}. Entiteten må ha en gyldig tilstandsverdi."
|
||||||
},
|
},
|
||||||
"entity_value_conversion_failed": {
|
"entity_value_conversion_failed": {
|
||||||
"message": "Kan ikke konvertere verdi '{raw_value}' fra '{entity_id}' ({attribute}) til {expected_type}. Kontroller at entiteten gir en kompatibel verdi."
|
"message": "Kan ikke konvertere verdi {raw_value} fra {entity_id} ({attribute}) til {expected_type}. Kontroller at entiteten gir en kompatibel verdi."
|
||||||
},
|
},
|
||||||
"capacity_required_for_percent": {
|
"capacity_required_for_percent": {
|
||||||
"message": "battery_capacity_kwh er påkrevd når nåværende eller mål-SoC oppgis i prosent."
|
"message": "battery_capacity_kwh er påkrevd når nåværende eller mål-SoC oppgis i prosent."
|
||||||
|
|
|
||||||
|
|
@ -1271,7 +1271,7 @@
|
||||||
"message": "array_fields kan alleen gebruikt worden met output_format: array_of_arrays. Wijzig het uitvoerformaat of verwijder array_fields."
|
"message": "array_fields kan alleen gebruikt worden met output_format: array_of_arrays. Wijzig het uitvoerformaat of verwijder array_fields."
|
||||||
},
|
},
|
||||||
"invalid_array_fields": {
|
"invalid_array_fields": {
|
||||||
"message": "De waarde {template} voor array_fields is ongeldig. Veldnamen moeten tussen accolades staan, bijv. '{start_time}, {price_per_kwh}, {level}'."
|
"message": "De waarde {template} voor array_fields is ongeldig. Veldnamen moeten tussen accolades staan, bijv. {start_time}, {price_per_kwh}, {level}."
|
||||||
},
|
},
|
||||||
"min_segment_exceeds_duration": {
|
"min_segment_exceeds_duration": {
|
||||||
"message": "min_segment_duration ({min_segment_minutes} min) mag de totale duur ({duration_minutes} min) niet overschrijden. Verklein min_segment_duration of vergroot duration."
|
"message": "min_segment_duration ({min_segment_minutes} min) mag de totale duur ({duration_minutes} min) niet overschrijden. Verklein min_segment_duration of vergroot duration."
|
||||||
|
|
@ -1298,19 +1298,19 @@
|
||||||
"message": "must_finish_by kan niet worden gecombineerd met eindgrensparameters ({params}). Gebruik must_finish_by alleen — het stelt het zoekeinde automatisch in op de deadline."
|
"message": "must_finish_by kan niet worden gecombineerd met eindgrensparameters ({params}). Gebruik must_finish_by alleen — het stelt het zoekeinde automatisch in op de deadline."
|
||||||
},
|
},
|
||||||
"invalid_entity_reference": {
|
"invalid_entity_reference": {
|
||||||
"message": "'{reference}' is geen geldige entiteitsreferentie. Gebruik het formaat 'domain.entity_id' of 'domain.entity_id@attribuut'."
|
"message": "{reference} is geen geldige entiteitsreferentie. Gebruik het formaat 'domain.entity_id' of 'domain.entity_id@attribuut'."
|
||||||
},
|
},
|
||||||
"entity_not_found": {
|
"entity_not_found": {
|
||||||
"message": "Entiteit '{entity_id}' niet gevonden. Controleer of de entiteit bestaat en beschikbaar is."
|
"message": "Entiteit {entity_id} niet gevonden. Controleer of de entiteit bestaat en beschikbaar is."
|
||||||
},
|
},
|
||||||
"entity_attribute_not_found": {
|
"entity_attribute_not_found": {
|
||||||
"message": "Entiteit '{entity_id}' heeft geen attribuut '{attribute}'."
|
"message": "Entiteit {entity_id} heeft geen attribuut {attribute}."
|
||||||
},
|
},
|
||||||
"entity_state_unavailable": {
|
"entity_state_unavailable": {
|
||||||
"message": "Entiteit '{entity_id}' heeft status '{state}'. De entiteit moet een geldige statuswaarde hebben."
|
"message": "Entiteit {entity_id} heeft status {state}. De entiteit moet een geldige statuswaarde hebben."
|
||||||
},
|
},
|
||||||
"entity_value_conversion_failed": {
|
"entity_value_conversion_failed": {
|
||||||
"message": "Kan waarde '{raw_value}' van '{entity_id}' ({attribute}) niet converteren naar {expected_type}. Controleer of de entiteit een compatibele waarde biedt."
|
"message": "Kan waarde {raw_value} van {entity_id} ({attribute}) niet converteren naar {expected_type}. Controleer of de entiteit een compatibele waarde biedt."
|
||||||
},
|
},
|
||||||
"capacity_required_for_percent": {
|
"capacity_required_for_percent": {
|
||||||
"message": "battery_capacity_kwh is vereist wanneer huidige of doel-SoC in procenten wordt opgegeven."
|
"message": "battery_capacity_kwh is vereist wanneer huidige of doel-SoC in procenten wordt opgegeven."
|
||||||
|
|
|
||||||
|
|
@ -1271,7 +1271,7 @@
|
||||||
"message": "array_fields kan bara användas med output_format: array_of_arrays. Ändra utdataformatet eller ta bort array_fields."
|
"message": "array_fields kan bara användas med output_format: array_of_arrays. Ändra utdataformatet eller ta bort array_fields."
|
||||||
},
|
},
|
||||||
"invalid_array_fields": {
|
"invalid_array_fields": {
|
||||||
"message": "Värdet {template} för array_fields är ogiltigt. Fältnamn måste omges av klammerparenteser, t.ex. '{start_time}, {price_per_kwh}, {level}'."
|
"message": "Värdet {template} för array_fields är ogiltigt. Fältnamn måste omges av klammerparenteser, t.ex. {start_time}, {price_per_kwh}, {level}."
|
||||||
},
|
},
|
||||||
"min_segment_exceeds_duration": {
|
"min_segment_exceeds_duration": {
|
||||||
"message": "min_segment_duration ({min_segment_minutes} min) får inte överstiga den totala varaktigheten ({duration_minutes} min). Minska min_segment_duration eller öka duration."
|
"message": "min_segment_duration ({min_segment_minutes} min) får inte överstiga den totala varaktigheten ({duration_minutes} min). Minska min_segment_duration eller öka duration."
|
||||||
|
|
@ -1298,19 +1298,19 @@
|
||||||
"message": "must_finish_by kan inte kombineras med slutgränsparametrar ({params}). Använd must_finish_by ensamt — det sätter sökslutet till deadline automatiskt."
|
"message": "must_finish_by kan inte kombineras med slutgränsparametrar ({params}). Använd must_finish_by ensamt — det sätter sökslutet till deadline automatiskt."
|
||||||
},
|
},
|
||||||
"invalid_entity_reference": {
|
"invalid_entity_reference": {
|
||||||
"message": "'{reference}' är inte en giltig entitetsreferens. Använd formatet 'domain.entity_id' eller 'domain.entity_id@attribut'."
|
"message": "{reference} är inte en giltig entitetsreferens. Använd formatet 'domain.entity_id' eller 'domain.entity_id@attribut'."
|
||||||
},
|
},
|
||||||
"entity_not_found": {
|
"entity_not_found": {
|
||||||
"message": "Entitet '{entity_id}' hittades inte. Kontrollera att entiteten finns och är tillgänglig."
|
"message": "Entitet {entity_id} hittades inte. Kontrollera att entiteten finns och är tillgänglig."
|
||||||
},
|
},
|
||||||
"entity_attribute_not_found": {
|
"entity_attribute_not_found": {
|
||||||
"message": "Entitet '{entity_id}' har inte attributet '{attribute}'."
|
"message": "Entitet {entity_id} har inte attributet {attribute}."
|
||||||
},
|
},
|
||||||
"entity_state_unavailable": {
|
"entity_state_unavailable": {
|
||||||
"message": "Entitet '{entity_id}' har tillståndet '{state}'. Entiteten måste ha ett giltigt tillståndsvärde."
|
"message": "Entitet {entity_id} har tillståndet {state}. Entiteten måste ha ett giltigt tillståndsvärde."
|
||||||
},
|
},
|
||||||
"entity_value_conversion_failed": {
|
"entity_value_conversion_failed": {
|
||||||
"message": "Kan inte konvertera värdet '{raw_value}' från '{entity_id}' ({attribute}) till {expected_type}. Kontrollera att entiteten ger ett kompatibelt värde."
|
"message": "Kan inte konvertera värdet {raw_value} från {entity_id} ({attribute}) till {expected_type}. Kontrollera att entiteten ger ett kompatibelt värde."
|
||||||
},
|
},
|
||||||
"capacity_required_for_percent": {
|
"capacity_required_for_percent": {
|
||||||
"message": "battery_capacity_kwh krävs när nuvarande eller mål-SoC anges i procent."
|
"message": "battery_capacity_kwh krävs när nuvarande eller mål-SoC anges i procent."
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "Tibber Price Information & Ratings",
|
"name": "Tibber Price Information & Ratings",
|
||||||
"homeassistant": "2026.4.0",
|
"homeassistant": "2026.4.4",
|
||||||
"hacs": "2.0.5"
|
"hacs": "2.0.5"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,4 @@ ruff>=0.15.15,<0.16.0
|
||||||
zlib_ng>=1.0.0,<1.1.0
|
zlib_ng>=1.0.0,<1.1.0
|
||||||
isal>=1.8.0,<1.9.0
|
isal>=1.8.0,<1.9.0
|
||||||
# Must match HA_VERSION in scripts/setup/bootstrap exactly (1:1 mapping)
|
# Must match HA_VERSION in scripts/setup/bootstrap exactly (1:1 mapping)
|
||||||
pytest-homeassistant-custom-component==0.13.322
|
pytest-homeassistant-custom-component==0.13.325
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ uv pip install --requirement requirements.txt
|
||||||
|
|
||||||
# HA_VERSION can be overridden from the environment, e.g.:
|
# HA_VERSION can be overridden from the environment, e.g.:
|
||||||
# HA_VERSION=2026.4.1 script/bootstrap
|
# HA_VERSION=2026.4.1 script/bootstrap
|
||||||
HA_VERSION=${HA_VERSION:-"2026.4.1"}
|
HA_VERSION=${HA_VERSION:-"2026.4.4"}
|
||||||
HA_CORE_BASE_URL="https://raw.githubusercontent.com/home-assistant/core/${HA_VERSION}"
|
HA_CORE_BASE_URL="https://raw.githubusercontent.com/home-assistant/core/${HA_VERSION}"
|
||||||
HA_TMP_DIR="$HOME/.ha_requirements"
|
HA_TMP_DIR="$HOME/.ha_requirements"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -297,3 +297,107 @@ async def test_schedule_handler_adds_per_task_comparison_details(monkeypatch: py
|
||||||
assert "comparison_price_min" in comparison
|
assert "comparison_price_min" in comparison
|
||||||
assert "comparison_price_max" in comparison
|
assert "comparison_price_max" in comparison
|
||||||
assert "comparison_window_end" in comparison
|
assert "comparison_window_end" in comparison
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_block_handler_preserves_service_search_data(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""Block handler must pass resolved call data (not coordinator data) into search helpers."""
|
||||||
|
intervals = _make_intervals([10.0, 11.0, 12.0, 13.0])
|
||||||
|
fake_tuple = _build_fake_entry_and_coordinator(intervals)
|
||||||
|
deadline = datetime(2026, 1, 1, 8, 0, tzinfo=UTC)
|
||||||
|
fixed_start = datetime(2026, 1, 1, 0, 0, tzinfo=UTC)
|
||||||
|
|
||||||
|
monkeypatch.setattr(block_module, "get_entry_and_data", lambda _hass, _entry_id: fake_tuple)
|
||||||
|
monkeypatch.setattr(block_module, "resolve_home_timezone", lambda _coord, _home_id: "UTC")
|
||||||
|
|
||||||
|
def _validate_search_params(call_data: dict[str, Any]) -> None:
|
||||||
|
assert call_data["must_finish_by"] == deadline
|
||||||
|
assert call_data["include_current_interval"] is False
|
||||||
|
|
||||||
|
def _apply_must_finish_by(call_data: dict[str, Any], _home_tz: Any) -> tuple[dict[str, Any], datetime]:
|
||||||
|
assert call_data["must_finish_by"] == deadline
|
||||||
|
modified = dict(call_data)
|
||||||
|
modified["search_end"] = deadline
|
||||||
|
modified.pop("must_finish_by", None)
|
||||||
|
return modified, deadline
|
||||||
|
|
||||||
|
def _resolve_search_range(call_data: dict[str, Any], _now: datetime, _home_tz: Any) -> tuple[datetime, datetime]:
|
||||||
|
assert call_data["include_current_interval"] is False
|
||||||
|
assert call_data["search_end"] == deadline
|
||||||
|
return fixed_start, deadline
|
||||||
|
|
||||||
|
async def _fetch_intervals(*_args: Any, **_kwargs: Any) -> tuple[list[dict[str, Any]], bool]:
|
||||||
|
return [], False
|
||||||
|
|
||||||
|
monkeypatch.setattr(block_module, "validate_search_params", _validate_search_params)
|
||||||
|
monkeypatch.setattr(block_module, "apply_must_finish_by", _apply_must_finish_by)
|
||||||
|
monkeypatch.setattr(block_module, "resolve_search_range", _resolve_search_range)
|
||||||
|
monkeypatch.setattr(block_module, "async_fetch_service_intervals", _fetch_intervals)
|
||||||
|
|
||||||
|
call = SimpleNamespace(
|
||||||
|
hass=object(),
|
||||||
|
data={
|
||||||
|
"duration": timedelta(hours=1),
|
||||||
|
"use_base_unit": True,
|
||||||
|
"must_finish_by": deadline,
|
||||||
|
"include_current_interval": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = cast("dict[str, Any]", await handle_find_cheapest_block(cast("ServiceCall", call)))
|
||||||
|
assert response["success"] is False
|
||||||
|
assert response["search_start"] == fixed_start.isoformat()
|
||||||
|
assert response["search_end"] == deadline.isoformat()
|
||||||
|
assert response["must_finish_by"] == deadline.isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_hours_handler_preserves_service_search_data(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""Hours handler must pass resolved call data (not coordinator data) into search helpers."""
|
||||||
|
intervals = _make_intervals([10.0, 11.0, 12.0, 13.0])
|
||||||
|
fake_tuple = _build_fake_entry_and_coordinator(intervals)
|
||||||
|
deadline = datetime(2026, 1, 1, 8, 0, tzinfo=UTC)
|
||||||
|
fixed_start = datetime(2026, 1, 1, 0, 0, tzinfo=UTC)
|
||||||
|
|
||||||
|
monkeypatch.setattr(hours_module, "get_entry_and_data", lambda _hass, _entry_id: fake_tuple)
|
||||||
|
monkeypatch.setattr(hours_module, "resolve_home_timezone", lambda _coord, _home_id: "UTC")
|
||||||
|
|
||||||
|
def _validate_search_params(call_data: dict[str, Any]) -> None:
|
||||||
|
assert call_data["must_finish_by"] == deadline
|
||||||
|
assert call_data["include_current_interval"] is False
|
||||||
|
|
||||||
|
def _apply_must_finish_by(call_data: dict[str, Any], _home_tz: Any) -> tuple[dict[str, Any], datetime]:
|
||||||
|
assert call_data["must_finish_by"] == deadline
|
||||||
|
modified = dict(call_data)
|
||||||
|
modified["search_end"] = deadline
|
||||||
|
modified.pop("must_finish_by", None)
|
||||||
|
return modified, deadline
|
||||||
|
|
||||||
|
def _resolve_search_range(call_data: dict[str, Any], _now: datetime, _home_tz: Any) -> tuple[datetime, datetime]:
|
||||||
|
assert call_data["include_current_interval"] is False
|
||||||
|
assert call_data["search_end"] == deadline
|
||||||
|
return fixed_start, deadline
|
||||||
|
|
||||||
|
async def _fetch_intervals(*_args: Any, **_kwargs: Any) -> tuple[list[dict[str, Any]], bool]:
|
||||||
|
return [], False
|
||||||
|
|
||||||
|
monkeypatch.setattr(hours_module, "validate_search_params", _validate_search_params)
|
||||||
|
monkeypatch.setattr(hours_module, "apply_must_finish_by", _apply_must_finish_by)
|
||||||
|
monkeypatch.setattr(hours_module, "resolve_search_range", _resolve_search_range)
|
||||||
|
monkeypatch.setattr(hours_module, "async_fetch_service_intervals", _fetch_intervals)
|
||||||
|
|
||||||
|
call = SimpleNamespace(
|
||||||
|
hass=object(),
|
||||||
|
data={
|
||||||
|
"duration": timedelta(hours=1),
|
||||||
|
"use_base_unit": True,
|
||||||
|
"must_finish_by": deadline,
|
||||||
|
"include_current_interval": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = cast("dict[str, Any]", await handle_find_cheapest_hours(cast("ServiceCall", call)))
|
||||||
|
assert response["success"] is False
|
||||||
|
assert response["search_start"] == fixed_start.isoformat()
|
||||||
|
assert response["search_end"] == deadline.isoformat()
|
||||||
|
assert response["must_finish_by"] == deadline.isoformat()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue