Cześć, właśnie rozpocząłem swoją historię z HomeAssistant. Mam integracje z Pstryk od https://github.com/kubass4/Pstryk-all-in-one . Mam też zintegrowany klimatyzator oraz bojler Ariston Hybrid. Napotkałem problem jak zrobić scenariusz lub pomocnika aby z wyfiltrowanych najtańszych godzin zakupu prądu uruchamiał mi grzanie wody w trybie boost. Fajnie by było też sterować klimą przy tańszych godzinach np. obniżać temperaturę oraz przyszłościowo na lato uruchamiać grzanie wody w basenie. Najpierw jednak chciałbym skonfigurować ten bojler. Pomoże ktoś jak to zrobić?
Witam, rozumiem że masz już zintegrowana klimę i bojler tak jak piszesz i jesteś w stanie przez HA uruchomić i zmieniać parametry.
Mam jeszcze pytanie narazie do bojlera ile czasu dziennie średnio grzejesz wodę to pomoże nam ustalić ile trzeba ci godzin najtańszego prądu chyba żeby chcesz grzać nie ciągle tylko w najniższych stawkach ale też nam się przyda ten średni czas. Dodatkowo jak jest definicja taniego prądu chodzi o poniżej średniej dziennej poniżej jakieś kwoty czy jeszcze inaczej?
Mam takie coś:
Chciałbym np ustawić sobie tryb Eco (wtedy działa tylko pompa ciepła) przez cały czas, a w tych najtańszych godzinach odpalać boost na pełną moc. Dobrze by było też odczytać temp. np o godzinie 16 i jeśli będzie poniżej jakiegoś progu np. 66 stopni to dogrzać ją mimo wysokiej ceny.
Udało mi się testowo uruchomić automatyzację ale tam ręcznie musze wpisać próg cenowy, a wiadomo one będą się różniły, a nie chcę zostać bez wody, bo mnie żona zje
To najlepiej to zrobić w nodered specjalistą w tym nie jestem ale spróbuję pomóc ile mogę może ktoś kto się lepiej zna się włączy.
Ciężko zgadywać co tam jest dostępne bez posiadania konta, ale wygląda na to ze sensor “sensor.pstryk_aio_obecna_cena_zakupu_pradu” ma między innymi atrybut “is_cheap” - true/false. Ten próg zapewne ustawia się w opcjach integracji?
Możesz zrobić sobie w takim razie pomocnika “szablon sensora binarnego”
Utwórz pomocnika → Template → Sensor binarny
Nazwa jaka chcesz (np. Pstryk – teraz jest tanio) i jako stan ustawić szablon:
{% set frames = state_attr('sensor.pstryk_aio_obecna_cena_zakupu_pradu', 'price_today') %}
{% if frames %}
{% set now = now().isoformat() %}
{{ frames
| selectattr('start', '<=', now)
| selectattr('end', '>', now)
| selectattr('is_cheap', 'eq', true)
| list
| count > 0 }}
{% else %}
false
{% endif %}
Będzie miał stan “włączony” gdy aktualna cena prądu będzie w przedziale który ustawiłeś jako “tanio”. Możesz sobie wtedy zrobić automatyzację:
“kiedy” → Pstryk - teraz jest tanio" zmieni stan
“wykonaj” → wybierz opcję
- warunek → Pstryk - teraz jest tanio - włączony
akcja → bojler boost - włącz
akcja → klimatyzacja - włącz
… - warunek → Pstryk - teraz jest tanio - wyłączony
akcja → bojler boost - wyłącz
akcja → klimatyzacja - wyłacz
…
Tutaj można by się pokusić o jakiś inny szablon, który nie bierze wartości “is_cheap” z ustawień integracji, tylko sam sobie oszacuje kiedy jest tanio skoro jest dostępny cennik dla całego dnia, np. 30% najtańszych godzin z dnia, albo kiedy aktualna cena jest niższa niż średnia dla całego dnia…
Chyba nie ma tego atrybutu “is_cheap” nie wiem gdzie to sprawdzić, ale po stworzeniu pomocnika i nawet zmianie progów cenowych zawsze ma status wyłączony
Atrybuty encji możesz sprawdzić w Narzędzia deweloperskie - Stany
Wybierasz encję, którą chcesz podejrzeć i masz wszystko jak na dłoni.
Przykład dla klimy
To w “sensor.pstryk_aio_obecna_cena_zakupu_pradu” mam tylko takie coś
data_timestamp: 2026-02-02T18:41:40.373699+00:00
unit_of_measurement: PLN/kWh
device_class: monetary
icon: mdi:transmission-tower-import
friendly_name: Pstryk AIO Obecna cena zakupu prądu
A to okienko co pokazuje najniższe ceny z tego progu który ustawiam to był taki kod do stworzenia panelu:
type: custom:button-card
show_name: false
show_icon: false
styles:
card:
- background: rgba(255, 152, 0, 0.1)
- border: "2px solid #FF9800"
- border-radius: 16px
- padding: 10px
- box-shadow: var(--ha-card-box-shadow)
- box-sizing: border-box
grid:
- grid-template-areas: "'prices'"
- grid-template-columns: 1fr
- grid-template-rows: auto
custom_fields:
prices: |
[[[
const buyEntity = states['sensor.pstryk_aio_obecna_cena_zakupu_pradu'];
if (!buyEntity || !buyEntity.attributes.today_prices || buyEntity.attributes.today_prices.length === 0) {
return '<div style="text-align: center; color: var(--secondary-text-color); width: 100%; box-sizing: border-box; padding: 10px;">Brak danych o cenach.</div>';
}
const allPricesToday = buyEntity.attributes.today_prices;
let rangeSummaryHtml = '';
let pricesForDetailedList;
const cheapHours = allPricesToday.filter(p => p.is_cheap === true);
if (cheapHours.length > 0) {
pricesForDetailedList = [...cheapHours].sort((a, b) => new Date(a.start) - new Date(b.start));
} else if (allPricesToday.length > 0) {
pricesForDetailedList = [...allPricesToday]
.sort((a, b) => a.price - b.price)
.slice(0, 5)
.sort((a, b) => new Date(a.start) - new Date(b.start));
} else {
pricesForDetailedList = [];
}
if (cheapHours.length > 0) {
const sortedCheapHoursForRanges = [...cheapHours].sort((a, b) => new Date(a.start) - new Date(b.start));
const ranges = [];
if (sortedCheapHoursForRanges.length > 0) {
let currentRangeStart = new Date(sortedCheapHoursForRanges[0].start);
let currentRangeEnd = new Date(sortedCheapHoursForRanges[0].end);
for (let i = 1; i < sortedCheapHoursForRanges.length; i++) {
const nextHourStart = new Date(sortedCheapHoursForRanges[i].start);
const nextHourEnd = new Date(sortedCheapHoursForRanges[i].end);
if (nextHourStart.getTime() === currentRangeEnd.getTime()) {
currentRangeEnd = nextHourEnd;
} else {
ranges.push({ start: currentRangeStart, end: currentRangeEnd });
currentRangeStart = nextHourStart;
currentRangeEnd = nextHourEnd;
}
}
ranges.push({ start: currentRangeStart, end: currentRangeEnd });
}
const individualRangeHtmlElements = ranges.map(r => {
const sH = r.start.getHours().toString().padStart(2, '0');
const sM = r.start.getMinutes().toString().padStart(2, '0');
const eH = r.end.getHours().toString().padStart(2, '0');
const eM = r.end.getMinutes().toString().padStart(2, '0');
const rangeText = `${sH}:${sM}-${eH}:${eM}`;
return `<div style="background-color: rgba(0, 0, 0, 0.5); color: #FF9800; padding: 5px 15px; border-radius: 999px; font-weight: bold; font-size: 18px; border: 0px; margin: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
${rangeText}
</div>`;
}).join('');
rangeSummaryHtml = `
<div style="font-size: 20px; color: var(--primary-text-color); margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px dashed var(--divider-color); text-align: center; width: 100%; box-sizing: border-box;">
⚡️ Najtańsze godziny zakupu dziś ⚡️
<div style="display: flex; flex-direction: row; flex-wrap: wrap; justify-content: center; align-items: center; gap: 8px; margin-top: 10px;">
${individualRangeHtmlElements}
</div>
</div>`;
} else { // Nie ma godzin "is_cheap", używamy pricesForDetailedList (5 najtańszych)
if (pricesForDetailedList.length > 0) {
const rangesFromLowestPrices = []; // Tutaj będą połączone zakresy z 5 najtańszych
// pricesForDetailedList jest już posortowane chronologicznie
let currentRangeStart = new Date(pricesForDetailedList[0].start);
let currentRangeEnd = new Date(pricesForDetailedList[0].end);
for (let i = 1; i < pricesForDetailedList.length; i++) {
const nextHourStart = new Date(pricesForDetailedList[i].start);
const nextHourEnd = new Date(pricesForDetailedList[i].end);
if (nextHourStart.getTime() === currentRangeEnd.getTime()) {
currentRangeEnd = nextHourEnd;
} else {
rangesFromLowestPrices.push({ start: currentRangeStart, end: currentRangeEnd });
currentRangeStart = nextHourStart;
currentRangeEnd = nextHourEnd;
}
}
rangesFromLowestPrices.push({ start: currentRangeStart, end: currentRangeEnd });
const lowestPricesPillsHtml = rangesFromLowestPrices.map(r => {
const sH = r.start.getHours().toString().padStart(2, '0');
const sM = r.start.getMinutes().toString().padStart(2, '0');
const eH = r.end.getHours().toString().padStart(2, '0');
const eM = r.end.getMinutes().toString().padStart(2, '0');
const rangeText = `${sH}:${sM}-${eH}:${eM}`;
return `<div style="background-color: rgba(0, 0, 0, 0.5); color: #FF9800; padding: 5px 15px; border-radius: 999px; font-weight: bold; font-size: 18px; border: 0px; margin: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
${rangeText}
</div>`;
}).join('');
rangeSummaryHtml = `
<div style="font-size: 18px; color: var(--primary-text-color); margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px dashed var(--divider-color); text-align: center; width: 100%; box-sizing: border-box;">
Brak godzin oznaczonych jako tanie.
<br>Poniżej 5 najtańszych cen dziś:
<div style="display: flex; flex-direction: row; flex-wrap: wrap; justify-content: center; align-items: center; gap: 8px; margin-top: 10px;">
${lowestPricesPillsHtml}
</div>
</div>`;
} else {
rangeSummaryHtml = `<div style="font-size: 18px; color: var(--primary-text-color); margin-bottom: 10px; padding-bottom: 6px; border-bottom: 1px dashed var(--divider-color); text-align: center; width: 100%; box-sizing: border-box;">Brak danych o cenach do wyświetlenia.</div>`;
}
}
let priceListDetailedHtml = '';
if (pricesForDetailedList.length > 0) {
priceListDetailedHtml = pricesForDetailedList.map(p => {
const start = new Date(p.start);
const end = new Date(p.end);
const sH = start.getHours().toString().padStart(2, '0');
const sM = start.getMinutes().toString().padStart(2, '0');
const eH = end.getHours().toString().padStart(2, '0');
const eM = end.getMinutes().toString().padStart(2, '0');
const timeRangeDisplay = `${sH}:${sM}-${eH}:${eM}`;
return `<div style="display: flex; justify-content: space-between; align-items: center; padding: 4px 0; border-bottom: 1px solid var(--divider-color); width: 100%; box-sizing: border-box;">
<span style="color: #FF9800; flex-shrink: 0; margin-right: 8px;">⚡️ ${timeRangeDisplay}</span>
<span style="font-weight: 500; color: var(--primary-text-color); text-align: right; flex-grow: 1;">${p.price.toFixed(2)} PLN/kWh</span>
</div>`;
}).join('');
} else if (allPricesToday.length > 0) {
priceListDetailedHtml = '<div style="text-align: center; color: var(--secondary-text-color); width: 100%; box-sizing: border-box; padding: 5px 0;">Brak odpowiednich cen do wyświetlenia na liście.</div>';
}
return rangeSummaryHtml + priceListDetailedHtml;
]]]
No ok , ale masz jeszcze filtr stanów i tam pewnie aktualna cena jako surowa liczba np 0.30
Najprościej jak mi przychodzi do głowy, tworzysz pomocnika sensor progowy ,w którym ustawiasz górny i dolny limit, jako sensor wejściowy wstawiasz swój czujnik z ceną . Skonfigurowany dolny i górny limit - Włącza się, gdy wartość sensora wejściowego znajduje się w zakresie [dolny limit … górny limit]
Na podstawie tego czujnika włączaj grzanie
To może tak:
{% set entity = 'sensor.pstryk_aio_obecna_cena_zakupu_pradu' %}
{% set prices = state_attr(entity, 'today_prices') %}
{% set now_hour = now().strftime('%Y-%m-%dT%H:00:00') %}
{% if prices is not none and prices is iterable %}
{# Szukamy ramki dla aktualnej godziny #}
{% set current_frame = prices | selectattr('start', 'search', now_hour) | list | first %}
{% if current_frame is defined %}
{# Sprawdzamy czy flaga is_cheap jest prawdziwa #}
{{ current_frame.is_cheap == true }}
{% else %}
false
{% endif %}
{% else %}
false
{% endif %}
Ooo, to zaczęło trybić jak zmieniłem próg cenowy. Teraz pytanie jak to zrobić, chyba że już to będzie, że jeżeli nie będzie żadna godzina mieściła się w tym zakresie, który mam ustawiony to będzie wybierał sobie np. 5 godzin najtańszych z dnia?
Tam w tym button-card właśnie widziałem taką linijkę
} else { // Nie ma godzin "is_cheap", używamy pricesForDetailedList (5 najtańszych)
if (pricesForDetailedList.length > 0) {
const rangesFromLowestPrices = []; // Tutaj będą połączone zakresy z 5 najtańszych
// pricesForDetailedList jest już posortowane chronologicznie
let currentRangeStart = new Date(pricesForDetailedList[0].start);
let currentRangeEnd = new Date(pricesForDetailedList[0].end);
i to pewnie ona za to odpowiada (nie znam się ale się wypowiem
, tak mi to wygląda)
{% set entity = 'sensor.pstryk_aio_obecna_cena_zakupu_pradu' %}
{% set prices = state_attr(entity, 'today_prices') %}
{% set now_hour = now().strftime('%Y-%m-%dT%H:00:00') %}
{% if prices is not none and prices is iterable and prices | count > 0 %}
{# 1. Znajdź ramkę danych dla aktualnej godziny #}
{% set current_frame = prices | selectattr('start', 'search', now_hour) | list | first %}
{% if current_frame is defined %}
{# 2. Sprawdź, czy są jakiekolwiek godziny oznaczone jako 'is_cheap' przez integrację #}
{% set cheap_hours = prices | selectattr('is_cheap', 'defined') | selectattr('is_cheap', 'eq', true) | list %}
{% if cheap_hours | count > 0 %}
{# Jeśli są godziny 'is_cheap' w opcjach, trzymaj się ich #}
{{ current_frame.is_cheap == true }}
{% else %}
{# Rezerwa (z Twojej karty): Wybierz 5 najtańszych godzin dnia #}
{% set sorted_prices = prices | sort(attribute='price') %}
{# Pobierz cenę 5-tej najtańszej godziny jako próg #}
{% set threshold_price = sorted_prices[4].price if sorted_prices | count >= 5 else sorted_prices[-1].price %}
{{ current_frame.price <= threshold_price }}
{% endif %}
{% else %}
{# Jeśli nie znaleziono ramki dla obecnej godziny #}
false
{% endif %}
{% else %}
{# Jeśli brak danych w atrybucie #}
false
{% endif %}
@Marcin4 dzięki wielkie! Dla mnie to czarna magia o co w tym wszystkim chodzi, ale powoli zaczynam podstawowe elementy rozumieć
Poczekałem specjalnie do rana, aż wejdzie nowa lista z godzinami i elegancko uruchomił się kod:
Co prawda przeleciał jedną godzinę za dużo, bo najtańsze godziny nieoznaczone w progu były 3, a on odpalił na 4 ale to i tak dla mnie jest cud, że działa tak jak wymarzyłem
alias: BOOST na 75 stopni
description: ""
triggers:
- trigger: state
entity_id:
- binary_sensor.pstryk_teraz_jest_tanio
to:
- "on"
conditions: []
actions:
- action: water_heater.set_operation_mode
metadata: {}
target:
entity_id: water_heater.bojler
device_id: 5a7d3488609b15903fd0c31376e63702
data:
operation_mode: BOOST
- action: water_heater.set_temperature
metadata: {}
target:
entity_id: water_heater.bojler
device_id: 5a7d3488609b15903fd0c31376e63702
data:
temperature: 75
mode: single
Nie roziem o co chodzi więc nie odpowiem, ale chyba to nie jest scena tylko automatyzacja. Takie nazewnictwo.
Tak tak chodziło mi o automatyzacje ![]()
W kodzie daliśmy mu polecenie: jeśli nic nie jest oznaczone “tanio”, to zawsze wybierz 5 najlepszych godzin. Skoro miałeś 3 super tanie, to sensor dobrał 2 kolejne “najmniej drogie”, żeby dobić do pięciu
{# Rezerwa (z Twojej karty): Wybierz 5 najtańszych godzin dnia #}
{% set sorted_prices = prices | sort(attribute='price') %}
{# Pobierz cenę 5-tej najtańszej godziny jako próg #}
{% set threshold_price = sorted_prices[4].price if sorted_prices | count >= 5 else sorted_prices[-1].price %}
jak chcesz inną ilość tych wartości do sprawdzenia to zmień kod w tym miejscu:
sorted_prices[X-1].price if sorted_prices | count >= X
czyli jak chcesz dla np trzech:
sorted_prices[2].price if sorted_prices | count >= 3
Pamiętaj że to jest sprawdzenie cen, pomocnik włączy się dla każdej godziny w której cena będzie mniejsza lub równa tej z ostatniej pozycji wyłuskanej tą listą. I to co pisałem wcześniej, tu możesz
sobie zmienić kod żeby wybierał np “poniżej średniej dnia”:
{% set entity = 'sensor.pstryk_aio_obecna_cena_zakupu_pradu' %}
{% set prices = state_attr(entity, 'today_prices') %}
{% set average = state_attr(entity, 'average_price') %}
{% set now_hour = now().strftime('%Y-%m-%dT%H:00:00') %}
{% if prices is not none and prices is iterable and prices | count > 0 %}
{# 1. Znajdź ramkę danych dla aktualnej godziny #}
{% set current_frame = prices | selectattr('start', 'search', now_hour) | list | first %}
{% if current_frame is defined %}
{# 2. Sprawdź, czy są jakiekolwiek godziny oznaczone jako 'is_cheap' przez integrację #}
{% set cheap_hours = prices | selectattr('is_cheap', 'defined') | selectattr('is_cheap', 'eq', true) | list %}
{% if cheap_hours | count > 0 %}
{# Jeśli cena spełnia warunek progu z ustawień Pstryk AIO #}
{{ current_frame.is_cheap == true }}
{% else %}
{# REZERWA: Jeśli nic nie jest 'is_cheap', sprawdź czy cena jest poniżej średniej #}
{% if average is not none %}
{{ current_frame.price <= average }}
{% else %}
{# Gdyby atrybut average_price był pusty, wybierz 5 najtańszych (bezpiecznik) #}
{% set sorted_prices = prices | sort(attribute='price') %}
{% set threshold_price = sorted_prices[4].price if sorted_prices | count >= 5 else sorted_prices[-1].price %}
{{ current_frame.price <= threshold_price }}
{% endif %}
{% endif %}
{% else %}
false
{% endif %}
{% else %}
false
{% endif %}
tutaj jak zmenisz
{{ current_frame.price <= average }}
na
{{ current_frame.price <= 0.7*average }}
to będzie wybierał 30% niższe niż średnia
No i co mamy Ci powiedzieć? ![]()
Chodziło mi czy dobrze to zrobiłem
Ale z tego co widzę wszystko na chwilę obecną śmiga ![]()
Dziękuję bardzo za pomoc, bo sam w życiu bym tego nie ogarnął ![]()
Dobrze zrobiłeś, masz osobną automatyzację na włączanie, osobną na wyłączanie…
Jeśli w “wyzwalaczu” nie wybierzesz stanu, to wtedy w akcjach możesz sprawdzić czy to było “włączenie” czy “wyłączenie” i będziesz miał jedną, ale to już jak wolisz.
Czytam i nie będąc programistą w żadnym calu uważam, że pomysł z wykorzystaniem sensora progowego, który zaproponował @isom1266 byłby by najbardziej elastycznym wykorzystaniem go w automatyzacji dla tej zmiennej ceny.
No ja uważam że stały sensor progowy wcale dobry nie jest…
Wyobraźmy sobie, że sensor progowy jest ustawiony na zakres 0,20 PLN – 0,40 PLN.
- Dzień tani: Wszystko działa, sensor się włącza.
- Dzień drogi (np. zima, bezwietrznie): Najniższa cena w ciągu dnia to 0,60 PLN - Sensor progowy nie włączy się ani razu
- Jest środek lata i ceny spadają do 0,05 PLN (nadprodukcja z PV) - sensor progowy… wyłączy się, bo cena wypadła poniżej dolnego limitu z zakresu sensora…
Oczywiście że dolny limit można ustawić na 0 żeby już nie wpaść w punkt 3 co było by absurdem, ale tak czy inaczej…







