Daikin Perfera 30 ja 40

puuteknikko

Vakionaama
Tämä on kommentti johon voi yhtyä. Daikinia on vaikea pitää samoissa asetuksissa jos yö-ja päivälämpötilan ero on suuri. Päivällä laite ei älyä pysähtyä vaikka asunnon lämpötila olisi useamman asteen yli pyynnin vaan se hönkii jollain teholla aivan turhaan. @Luukun käyristäkin näkee että vaikka pyynti on alhainen (20,5) niin laite käy katkokäyntiä ja kuluttaa sähköä satoja watteja vaikkei tarvitsisi asunnon lämpötilan (yli 22) puolesta lämmittää ollenkaan.
Alkuperäinen kysymykseni oli että onko tämä vika vai ominaisuus, näyttää siltä että on valitettavasti ominaisuus.
Käyhän ne muutkin samalla tavalla katkokäyntiä kun lämmitystarve seilaa on/off etenkin näin syksyisin kun ulkolämpötilan vaihtelu on suurta ja auringonpaiste antaa vielä lämpöä jonkin verran. Ei se mitään valtavia määriä kuluta. Oma P40 on käynyt katkoa läpi viime yön ja esim. kuuden viime tunnin keskiteho Shellyn mukaan on 180 W.
 

Sampo22

Aktiivinen jäsen
Käyhän ne muutkin samalla tavalla katkokäyntiä kun lämmitystarve seilaa on/off etenkin näin syksyisin kun ulkolämpötilan vaihtelu on suurta ja auringonpaiste antaa vielä lämpöä jonkin verran. Ei se mitään valtavia määriä kuluta. Oma P40 on käynyt katkoa läpi viime yön ja esim. kuuden viime tunnin keskiteho Shellyn mukaan on 180 W.
Juu näin se on. Mutta minun ihmettelyni ei koskekaan lämmitystä tai katkokäyntiä yöllä. Avainsana on juuri tuo ”lämmitystarve”. Jos pumpun asetus on 22 astetta ja talossa päivällä auringon vaikutuksesta 25 astetta niin miksi molemmat yksiköt vielä pyörii? Tämän lienee suunnitellut sama insinööri joka keksi että puhallusnopeutta ei voi säätää manuaalisesti vaan on 2 automaattiasentoa joista toisessa voi asettaa maksimipuhalluksen.
 
.... @Luukun käyristäkin näkee että vaikka pyynti on alhainen (20,5) niin laite käy katkokäyntiä ja kuluttaa sähköä satoja watteja vaikkei tarvitsisi asunnon lämpötilan (yli 22) puolesta lämmittää ollenkaan.
Alkuperäinen kysymykseni oli että onko tämä vika vai ominaisuus, näyttää siltä että on valitettavasti ominaisuus.
Plus, että eikös Luukulla ollut monenmoista automaatiota tuota säätämässä? Oma Stylish on täysin natu ja ainoastaan tarveohjaus rajoittaa sen maksimitehojat tällä hetkellä.

Pitäisikin tehdä jokin empiirinen koe, jossa testaisi että miten paljon täytyy kämpän ylilämmetä että pumppu lopettaisi lämpimän puhaltamisen, mutta ihan niin paljon ei tällä hetkellä ole aikaa ja mielenkiintoa, ongelma poistuu kun talvi lähestyy...

Mutta vähän samaan kategoriaan menee tämän pumpun takkatoiminto, se ei kunnolla lähde kierrättämään ilmaa ellet lämmitä ihan hulluna sitä takkaa.
 

tuna

Vakionaama
Juu näin se on. Mutta minun ihmettelyni ei koskekaan lämmitystä tai katkokäyntiä yöllä. Avainsana on juuri tuo ”lämmitystarve”. Jos pumpun asetus on 22 astetta ja talossa päivällä auringon vaikutuksesta 25 astetta niin miksi molemmat yksiköt vielä pyörii? Tämän lienee suunnitellut sama insinööri joka keksi että puhallusnopeutta ei voi säätää manuaalisesti vaan on 2 automaattiasentoa joista toisessa voi asettaa maksimipuhalluksen.
Tarkkailepa mitä pumpun oma huonelämpömittaus kertoo. Ei se vain piruuttaan toimi tietyllä lailla vaan ihan lämpötilasäätimestä on kyse. Tässä omasta pumpusta eiliseltä. Kone näyttäisi käyvän "tyhjäkäynnillä" kun ollaan aste yli tavoitteen, ja käyvän katkolla kun kaksi yli. Mutta mittauksen pudotessa taas asteen yli jatkaa "tyhjäkäynnillä". Kun mittauspaikka on sisäyksikössä itsessään, se ei tarkkaan pysty reguloimaan miten lämmintä jossain muualla on jonne esim aurinko paistaa. Kokonaisen asteen resoluutio kuulostaa äkkiseltään karkealta mutta ei tuolla anturin sijoituksella järkeä oikein tarkemmassakaan olisi.

Kääntöpuolena tämä P40 pitää huonelämpötilan paljon paremmin haluttuna silloin kun oikeasti on kylmä, kuin aiempi FD25. Siis tilanteessa jossa lämmitystehoa ei ole liikaa keliin verrattuna.
 

Liitteet

  • Screenshot_20250930_080822_Home Assistant.jpg
    Screenshot_20250930_080822_Home Assistant.jpg
    61,8 KB · Katsottu: 37

puuteknikko

Vakionaama
Ylipäätään laitteen sijoituspaikka vaikuttaa todella paljon, ja niin se on kaikilla laitteilla, joissa huonelämpötilaa mitataan sisäyksikössä olevalla anturilla.
 

Luukku

Vakionaama
Plus, että eikös Luukulla ollut monenmoista automaatiota tuota säätämässä?
Nyt ei ole ollut mitääm muita automaatioita kuin sulatuksiin liittyvät ja niitä ei ole vielä ollut. Ihan natuna toimivat.
Eilen oli hyvä esimerkki miten ihmisen lämmönaistiminenkin vaihtelee vrk ajan mukaan. Rouva ilmoitti illalla mulle töihin, että "täällä on vähän viileetä". HA:n käppyrä näytti, että oli lämpimämpää kuin aamulla klo 8. Nyt on nollassa ja samalla pyynnillä mennään kuin eilen, pumppu puhaltelee selvästi kovemmalla puhalluksella kuin eilen. Vaikka nuo katkoo ja "kuluttaa turhaan" niin hyvin tasaisena lämpötila pysyy.
 

julli100

Aktiivinen jäsen
Mitsussa lämmityksellä vähentää 2 astetta anturin lukemasta ja jäähdytyksellä lisää 2 astetta. Kun olen ulostanut anturin 3 metrin päähän laitteen sisäkaluista tämä täytyy ottaa huomioon
 

Luukku

Vakionaama
Tuli tuossa mieleen tosta katkokäynnistä ja sähkönkulutuksesta, että jos huomaa pumpun käyvän katkoa niin on ensiarvoisen tärkeää olla econo, UY hiljainen tila tai tarveohjaus päällä niin ei ryntää tehot liian korkealle, kun UY käynnistyy. Stylish käy nyt 15 min pätkiä ja teho pomppaa vaan 300-400W, kun käynnistyy katkon jälkeen.
 

Sampo22

Aktiivinen jäsen
Tarkkailepa mitä pumpun oma huonelämpömittaus kertoo. Ei se vain piruuttaan toimi tietyllä lailla vaan ihan lämpötilasäätimestä on kyse. Tässä omasta pumpusta eiliseltä. Kone näyttäisi käyvän "tyhjäkäynnillä" kun ollaan aste yli tavoitteen, ja käyvän katkolla kun kaksi yli. Mutta mittauksen pudotessa taas asteen yli jatkaa "tyhjäkäynnillä". Kun mittauspaikka on sisäyksikössä itsessään, se ei tarkkaan pysty reguloimaan miten lämmintä jossain muualla on jonne esim aurinko paistaa. Kokonaisen asteen resoluutio kuulostaa äkkiseltään karkealta mutta ei tuolla anturin sijoituksella järkeä oikein tarkemmassakaan olisi.

Kääntöpuolena tämä P40 pitää huonelämpötilan paljon paremmin haluttuna silloin kun oikeasti on kylmä, kuin aiempi FD25. Siis tilanteessa jossa lämmitystehoa ei ole liikaa keliin verrattuna.
Nyt on pilvinen päivä ja ulkona +10 astetta joten alkuperäinen selvittely aurinkolämmön vaikutuksesta ei tänään etene. Laitoin nyt kumminkin aamulla pyynnin 21 astetta ja puhallus 4. Sisäyksikön näyttämä mittaus vaihtelee 22-23 välillä, viereen asennettu oma anturi näyttää 22,5 astetta. Puhallus näyttää audiovisuaalisesti vaihtelevan välillä 2-3 ja ulospuhalluslämpötila 33-35 astetta. 6 metrin päässä pumpusta lämpötila on haluttu eli 22 astetta. Eli sinänsä varmaan asianmukaista toimintaa. Saa nyt puksuttaa näillä asetuksilla aamuun asti jolloin näkee onko talo jäähtynyt vai lämmöt pysyneet lähellä 22:ta.
 

Sampo22

Aktiivinen jäsen
Nyt kun ulkolämpötila on yötäpäivää lähellä +10 astetta ja sisätilan tavoitelämpötila 22 astetta niin pumppu tuppaa käymään jatkuvaa katkokäyntiä. Tämän sai eliminoitua siten, että laitoin pyynnin 25 astetta ja tarveohjauksen 40%. Nyt ei katko enää ollenkaan ja sisäyksikön puhallusilma pysyy tasaisesti 31-32 asteessa. Tottelee myös manuaalisia puhallinnopeuksia vaikka vitoseen asti. Onectan mukaan sähköä kuluu 20-30% vähemmän verrattuna katkokäyntiin. Kun ilmat viilenee niin täytyy varmaan hilata tarveohjausta ylöspäin.
 

Luukku

Vakionaama
Nyt kun ulkolämpötila on yötäpäivää lähellä +10 astetta ja sisätilan tavoitelämpötila 22 astetta niin pumppu tuppaa käymään jatkuvaa katkokäyntiä. Tämän sai eliminoitua siten, että laitoin pyynnin 25 astetta ja tarveohjauksen 40%. Nyt ei katko enää ollenkaan ja sisäyksikön puhallusilma pysyy tasaisesti 31-32 asteessa. Tottelee myös manuaalisia puhallinnopeuksia vaikka vitoseen asti. Onectan mukaan sähköä kuluu 20-30% vähemmän verrattuna katkokäyntiin. Kun ilmat viilenee niin täytyy varmaan hilata tarveohjausta ylöspäin.
Erinomainen huomio! Nyt pitää seurata lämpötiloja ja automaatiolla sitten sammuttaa pumppu joksikin aikaa, jos lämmöt nousee liikaa. Stylish yläkerrassa ollut pahempi katkoja, näytti nyt käynti paranevan. Ollut aikaisemminkin tuo tarveohjaus siellä päällä, mutta pyynti ollut paljon alempi.
 

Liitteet

  • IMG_6444.jpeg
    IMG_6444.jpeg
    77,5 KB · Katsottu: 27

Luukku

Vakionaama
Perferan käyntikin muuttui, ottoteho ja puhalluslämpö putosi, puhallus automaatilla ja nyt puhaltaa kovempaa.
Eli Daikinia pitääkin ajaa aina rajoitinta vasten :)
 

Liitteet

  • IMG_6446.jpeg
    IMG_6446.jpeg
    123,7 KB · Katsottu: 34

Sampo22

Aktiivinen jäsen
Perferan käyntikin muuttui, ottoteho ja puhalluslämpö putosi, puhallus automaatilla ja nyt puhaltaa kovempaa.
Eli Daikinia pitääkin ajaa aina rajoitinta vasten :)
Viime syksynä ja alkutalvesta käytin tuota tarveohjausrajoitusta niin pitkään kun lämmöt talossa riitti, jäi sellainen käsitys että sulatuksetkin harveni kun pumppu ei pääse ryntäilemään pyynnin perässä.
 

Luukku

Vakionaama
Yläkerran Stylish piti muuttaa on/off koneeksi automaatiolla, tulee liian lämmin. Pätkii se näin tietysti vähemmän.
 

Sampo22

Aktiivinen jäsen
Yläkerran Stylish piti muuttaa on/off koneeksi automaatiolla, tulee liian lämmin. Pätkii se näin tietysti vähemmän.
Tilanne korjaantuu itsekseen kunhan kelit kylmenee Meillä on pumppu rintamamiestalon eteisessä portaikon alapäässä ja lämpö pääsee nousemaan myös ylös ja riittää nyt molempiin kerroksiin noin 200 W/h sähkönkulutuksella. Yläkerran Ultimate on vain jäähdytyskäytössä kesällä.
 

Luukku

Vakionaama
Automaatiolla saa Perferaankin "Toshiban takkatilan", kun Ruuvin lämpötilan mittaus on uunin kanssa samassa huoneessa niin, jos uunin lämpö alkaa vaikuttaa riittävästi niin Perfera menee puhallukselle ja päinvastoin.
 

Luukku

Vakionaama
Tilanne korjaantuu itsekseen kunhan kelit kylmenee Meillä on pumppu rintamamiestalon eteisessä portaikon alapäässä ja lämpö pääsee nousemaan myös ylös ja riittää nyt molempiin kerroksiin noin 200 W/h sähkönkulutuksella. Yläkerran Ultimate on vain jäähdytyskäytössä kesällä.
Kyllä, yläkerrassa pysyy jatkuvalla käynnillä vasta alle 5 asteen pakkasilla, jos tarveohjaus on päällä.
 

Luukku

Vakionaama
Perfera tekee kerran tunnissa n.500W ryntäyksen. Varmaan jotain öljyjä liikutellaan?
 

Liitteet

  • IMG_6448.jpeg
    IMG_6448.jpeg
    62,8 KB · Katsottu: 34

Sampo22

Aktiivinen jäsen
Perfera tekee kerran tunnissa n.500W ryntäyksen. Varmaan jotain öljyjä liikutellaan?
Tuohon en osaa sanoa muuta kuin että meidän P40 ei tee tuollaista. Voisiko olla hetkellinen piikki puhalluksessa jos se on automaatilla, itse pidän nyt manuaalisesti kolmosella.
 

Luukku

Vakionaama
Tuohon en osaa sanoa muuta kuin että meidän P40 ei tee tuollaista. Voisiko olla hetkellinen piikki puhalluksessa jos se on automaatilla, itse pidän nyt manuaalisesti kolmosella.
Puhallus pysyy tasaisena, mutta ottoteho piikittelee, aina tehnyt tuota ja varmaan normaalia.
 

Sampo22

Aktiivinen jäsen
Perfera tekee kerran tunnissa n.500W ryntäyksen. Varmaan jotain öljyjä liikutellaan?
Meillä ei ole tarkkaa kulutusmittausta pumpussa mutta on Ruuvi-anturi ulospuhallussäleikössä. Kas kummaa, siinähän näkyy pieni lämpötilapiikki noin tunnin välein, liittyy varmaan tuohon tehopiikkiin. Jotain siinä tapahtuu mutta en tiedä mitä.
 

Liitteet

  • IMG_5113.png
    IMG_5113.png
    323,3 KB · Katsottu: 37

iro

Vakionaama
Meillä ei ole tarkkaa kulutusmittausta pumpussa mutta on Ruuvi-anturi ulospuhallussäleikössä. Kas kummaa, siinähän näkyy pieni lämpötilapiikki noin tunnin välein, liittyy varmaan tuohon tehopiikkiin. Jotain siinä tapahtuu mutta en tiedä mitä.
Myös Panassa minimitehoilla ottotehossa näkyy toistuvasti piikki neljän tunnin välein.
 

Liitteet

  • Screenshot_20251008-075613.jpg
    Screenshot_20251008-075613.jpg
    65,4 KB · Katsottu: 29

Luukku

Vakionaama
Automaatiolla saa Perferaankin "Toshiban takkatilan", kun Ruuvin lämpötilan mittaus on uunin kanssa samassa huoneessa niin, jos uunin lämpö alkaa vaikuttaa riittävästi niin Perfera menee puhallukselle ja päinvastoin.
Toimii näillä keleillä nyt varsin mukavasti. Saas nähdä sitten pakkasilla, kun uunin lämmittää.
Vähän piti korjata lämpötilarajoja, kun yläkerta tuntui aamulla liian lämpimältä.
Keittiön hukkalämpöön reagoi ja heitti Perferan puhallukselle.
Nyt on yläkerran koneellakin vähemmän katkoja ja käyntijakso pidempi, kun lämmitys menee päälle. Selvästi näissä laitteissa on tehtaan jäljiltä liian vähän älyä.
 
  • Tykkää
Reactions: iro

Sampo22

Aktiivinen jäsen
Onkos joku käyttänyt ”Ilmastoinnin kompensoinnin säätöä” ja toimiiko se? Löytyy Onectasta Asetukset/Lisätoiminnot -valikosta ja varmaan kapulastakin. Nimi jo itsessään on harhaanjohtava, kysymys lienee lämmityksen asetusarvon automaattisesta nostosta ulkolämpötilan laskiessa nollan alapuolelle.
 

Liitteet

  • IMG_5212.png
    IMG_5212.png
    207,5 KB · Katsottu: 54

Luukku

Vakionaama
Kahdella automaatiolla (sais ne kyllä yhteenkin) ohjaan nyt ilppejä, "fan only" ja "heat" automaatioissa lämpötilarajat missä vaihtaa ilpin moodia.
Hillitsee yläkerran pumpulta jatkuvaa katkoa hyvin ja lämmitysjakso on pidempi, kun tarvetta ilmenee. Alakerran pumppu reagoi hyvin kakluunin tai keittiön lisälämpöön eli siirtyy "Toshiban takka moodiin":)
Tein vielä yläkertaan lisäautomaation (aikataulu) millä sallii iltaisin korkeamman mukavuuslämmön, yöllä pitää olla viileämpää kuitenkin, jotta nukkuu paremmin.
 

Luukku

Vakionaama
Eilen aamupäivällä kakluunin lämmitys pudotti Perferan "takkatoiminnolle".
Varsin tarpeellinen automaatio näin, kun ulkona ollaan vielä plussalla ja yö/päivä lämpötilaerot suuria.
Yläkerran (Stylish) katkominen maltillisempaa kuin aikaisemmin.
 

Liitteet

  • IMG_6509.jpeg
    IMG_6509.jpeg
    113,5 KB · Katsottu: 43
  • IMG_6511.jpeg
    IMG_6511.jpeg
    49,3 KB · Katsottu: 40

Sampo22

Aktiivinen jäsen
Nyt kun ulkolämpötila huitelee 10 asteessa yötäpäivää niin pumpun automatiikka on vaikeuksissa. Itse olen ajellut pumppua ”rajoitinta vasten” asetuksilla 30 astetta, puhallus 3 ja tarveohjaus 40-50%. Näillä pumppu ei katko vaan puhaltaa tasaisesti 31-33 asteista, kulutus 300 W/h. ”Normiasetuksilla” tahtoo kauempana olevat tilat jäädä viileiksi kun automatiikka pienentää puhallusta.
 

Luukku

Vakionaama
Itse olen ajellut pumppua ”rajoitinta vasten” asetuksilla 30 astetta, puhallus 3 ja tarveohjaus 40-50%. Näillä pumppu ei katko vaan puhaltaa tasaisesti 31-33 asteista, kulutus 300 W/h.
Samat asetukset täälläkin. HA valvoo ja ohjaa, jos mennään lämpötilassa ala- tai ylärajalle. Pitäis varmaan se "Faikin" hommata, että sais tarveohjauksenkin automaation säädettäväksi. Tulee tarpeelliseksi sitten, kun ilmat kylmenee.
Saa nytkin hälytyksen luuriin, jos tarveohjaus rajoittaa liikaa, mutta pitää Onectan kautta sitten säätää.
 

Kaimo Ärräpää

Yleisinsinööri (amk)
Joko on tähän malliin löytynyt Sevice Manual? Tai suomeksi tehdasmanuaali? Kyllä se vain kiinnostaa heti insinörtillä tutustua tulevan pumppenin sisälmyksiin.

P.S. Joku Service Manual löytyi, mutta oli suunnilleen FXTMB -mallista!
 

Koelli

Aktiivinen jäsen
Rakensin Daikinille Home Assisantiin koneoppimismallin, joka kouluttaa ohjaamaan Demand Controllia ympäristömuuttujilla.

Mallissa otetaan huomioon:
  • Sisälämpötila
  • Sisälämpötila Delta
  • Ulkolämpötila
  • Ulkolämpötilan ennuste
Ominaisuuksia:
  • Valinta tavoitelämpötilalle, jota malli käyttää arvona, johon pyritään ja joka pyritään pitämään
  • Sulatuksen tunnistus -> ei opetusta, sulatuksen jälkeen 15min cooldown, jotta ei lähde ryntäämään
  • Ulkolämpötilan mukainen koneoppiminen 1 c askelin, joka tallennetaan pysyvään tiedostoon
  • Jäätämisen kriittinen alue -4 c - +4 c. Vältetään tiheä sulattelu rajoittamalla Demand Controllia
Roadmapilla:
  • Kastepisteen huomioiminen, jotta ulkokenno pidettäisiin kastepisteen yläpuolella
Kellään käyttöä tällaiselle?
 
Viimeksi muokattu:

heebo1974

Aktiivinen jäsen
Rakensin Daikinille Home Assisantiin koneoppimismallin, joka kouluttaa ohjaamaan Demand Controllia ympäristömuuttujilla.

Kellään käyttöä tällaiselle?
En tiedä, voihan se olla että tämä luo sen tarpeen. :)
Mitä vaatimuksia tämän käytölle on ? Toimiiko perus Daikinin lisäosilla, vai tarvitseeko esim. Faikinia ?
 

Koelli

Aktiivinen jäsen
En tiedä, voihan se olla että tämä luo sen tarpeen. :)
Mitä vaatimuksia tämän käytölle on ? Toimiiko perus Daikinin lisäosilla, vai tarvitseeko esim. Faikinia ?
Muistaakseni vanhemmilla wifi-adaptereilla Daikin antaa ulos tarveohjauksen? Tietenkin se on minimivaatimus. Yhtä lailla minimivaatimus on, että pystyt mittaamaan joltain Daikinin ulkopuoliselta anturilta lämpötiloja, koska Daikinin huonelämpötila-anturilla ei tee juuri mitään.

Faikinilla toteutin tämän, mutta ottaen huomioon yllä olevat, niin pitäisi toimia. Faikin tuo kylmäaineen lämpötilatiedot, joita käytän mittaamaan sulatusta, ja cooldownia, mutta sulatustiedon voi saada muillakin tavoilla.

HA:n joutuu lataamaan Pyscript:n HACS:sta, mutta muutoin helppo ottaa käyttöön.
 

Koelli

Aktiivinen jäsen
Tässä Python-koodi, joka tulee tallentaa polkuun /config/pyscript/ ja lisäksi automaatiot, joista toinen pakottaa Demand Controllia ja "vartioi" sitä, ja toinen suorittaa varsinaisen Demand Controllin. Tämän lisäksi tulee vielä tehdä helperit.

Huomionarvoista sekin, että tämä on nyt ollut käytössä peräti yhden vuorokauden itsellä, joten mitään takeita toimivuudesta ei ole :)

Helperit configuration.yaml:iin:
YAML:
input_number:
  daikin_setpoint:
    name: Daikin setpoint (°C)
    min: 15
    max: 25
    step: 0.1
    unit_of_measurement: "°C"

  daikin_kp:
    name: Kp (% per °C)
    min: 1
    max: 40
    step: 1
    unit_of_measurement: "%"

  daikin_kd:
    name: Kd (% per °C/h)
    min: 0
    max: 80
    step: 2
    unit_of_measurement: "%"

  daikin_load_gain:
    name: Load gain (% per Δ°C_out)
    min: 0
    max: 8
    step: 0.2
    unit_of_measurement: "%"

  daikin_forecast_weight:
    name: Forecast weight (0..1)
    min: 0.0
    max: 1.0
    step: 0.1
    mode: slider

  daikin_base:
    name: Base demand (%)
    min: 30
    max: 100
    step: 1
    unit_of_measurement: "%"

  daikin_step_limit:
    name: Max muutos / sykli (%)
    min: 1
    max: 30
    step: 1
    unit_of_measurement: "%"

  daikin_base_learn_rate:
    name: Base learn rate
    min: 0.0
    max: 0.5
    step: 0.05
    mode: box

  daikin_deadband:
    name: Deadband (°C)
    min: 0.0
    max: 0.5
    step: 0.05

Python:
# pyscript/daikin_ml.py
# Online-RLS (oppiva) ohjain Daikin demand-selectille.
# Ominaisuudet:
# - min 30 %, max 100 %
# - 6h ulkoennusteen keskiarvo feed-forwardina
# - pakotettu suunta-askel (aina vähintään step_limit kohti setpointtia deadbandin ulkopuolella)
# - deadband, step-limit, monotonisuus ja selectin pykälöinti
# - oppiminen (RLS) jäädytetään defrostin aikana (sensor.faikin_liquid < 20)
# - parametrisäilö input_text.daikin_rls_params (vain theta tallennetaan, ei P)
#
# VAIHDA NÄMÄ ENTITYT:
#   INDOOR  = "sensor.home_temp"                 # sisälämpötila
#   OUTDOOR = "sensor.outdoor_temp"              # ulkolämpötila
#   WEATHER = "weather.home"                     # weather.* jossa 'forecast'
#   SELECT  = "select.faikin_demand_control"     # Daikin demand-select
#   LIQUID  = "sensor.faikin_liquid"             # defrost-indikaattori (alle 20 = defrost)
#   INDOOR_RATE, SP_HELPER, STEP_LIMIT_HELPER, DEADBAND_HELPER, PARAMS_TXT: ks. alta

import json
import time
from math import isfinite

# --------- ENTITYT (VAIHDA OMIKSI) ----------
INDOOR = "sensor.living_room_lampotila"                 # <-- sisälämpötila
INDOOR_RATE = "sensor.sisalampotila_aula"  # derivative-sensori (°C/h)
OUTDOOR = "sensor.iv_tulo_lampotila"             # <-- ulkolämpötila
WEATHER = "weather.koti"                    # <-- weather.* josta forecast-attribuutti
SELECT = "select.faikin_demand_control"     # <-- Daikinin demand-select
LIQUID = "sensor.faikin_liquid"             # <-- defrost-indikaattori


SP_HELPER = "input_number.daikin_setpoint"
STEP_LIMIT_HELPER = "input_number.daikin_step_limit"
DEADBAND_HELPER = "input_number.daikin_deadband"

# TÄHÄN TALLENNETAAN OPPIMINEN (pysyväksi)
STORE_ENTITY = "pyscript.daikin_ml_params"

# ---------- SÄÄTÖNUPIT ----------
HORIZON_H  = 1.0      # tuntia; kuinka nopeasti pyritään korjaamaan virhettä
FORECAST_H = 6        # montako forecast-askelta mukaan
LAMBDA     = 0.995    # RLS-unkohduskerroin
P0         = 1e4
MIN_DEM    = 30.0
MAX_DEM    = 100.0
KAPPA      = 1.0      # kuinka aggressiivisesti virhettä korjataan (feed-forward taso)

# Defrost-cooldown
COOLDOWN_MINUTES = 15          # defrostin jälkeen hillitään nousua
COOLDOWN_STEP_UP = 3.0         # max ylös-muutos (%-yks/sykli) cooldownissa

# Icing band (staattinen raja) – -4…+4, cap 80 %
ICING_BAND_MIN = -4.0
ICING_BAND_MAX =  4.0
ICING_BAND_CAP = 80.0

# AUTO-TUNING rajat
AUTO_STEP_MIN  = 3.0    # %-yks / sykli
AUTO_STEP_MAX  = 20.0
AUTO_STEP_BASE = 10.0

AUTO_DB_MIN    = 0.05   # °C
AUTO_DB_MAX    = 0.50
AUTO_DB_BASE   = 0.10

# ---------- Konteksti per ulkolämpöaste ----------
def _context_key_for_outdoor(Tout: float) -> str:
    """Pyöristetään ulkolämpö 1°C tarkkuudelle, esim. -6.7 -> '-7'."""
    if not isfinite(Tout):
        return "nan"
    return str(int(round(Tout)))

# Moduulitason tila: mallit per aste
_theta_by_ctx   = {}   # esim. {"-7":[...], "0":[...], "3":[...]}
_P_by_ctx       = {}   # ei tallenneta pysyvästi, elää muistissa
_params_loaded  = False

_last_defrosting = None
_cooldown_until  = 0.0

# ---------- PYSYVÄ STORE pyscriptin state.persistillä ----------
state.persist(STORE_ENTITY)

def _load_params_from_store():
    """Lue per-aste thetat pysyvän entityn attribuuteista."""
    global _theta_by_ctx
    _theta_by_ctx = {}

    attrs = state.getattr(STORE_ENTITY) or {}
    tb = attrs.get("theta_by_ctx")
    if isinstance(tb, dict):
        for key, val in tb.items():
            if isinstance(val, (list, tuple)) and len(val) == 4:
                try:
                    th = [float(val[0]), float(val[1]), float(val[2]), float(val[3])]
                    _theta_by_ctx[str(key)] = th
                except Exception:
                    continue

    if _theta_by_ctx:
        log.info("Daikin ML: loaded %d contexts from %s", len(_theta_by_ctx), STORE_ENTITY)
    else:
        log.info("Daikin ML: no stored thetas in %s, starting fresh", STORE_ENTITY)

def _save_params_to_store():
    """Tallenna kaikkien kontekstien thetat pysyvän entityn attribuutiksi."""
    global _theta_by_ctx

    clean = {}
    for key, th in _theta_by_ctx.items():
        if not isinstance(th, (list, tuple)) or len(th) != 4:
            continue
        clean[str(key)] = [
            round(float(th[0]), 4),
            round(float(th[1]), 4),
            round(float(th[2]), 4),
            round(float(th[3]), 4),
        ]

    try:
        state.set(
            STORE_ENTITY,
            value=time.time(),
            theta_by_ctx=clean,
        )
        log.debug(
            "Daikin ML: stored %d contexts into %s (attr size ~%d chars)",
            len(clean),
            STORE_ENTITY,
            len(str(clean)),
        )
    except Exception as e:
        log.error("Daikin ML: error saving thetas to %s: %s", STORE_ENTITY, e)

def _init_context_params_if_needed():
    """Lataa per-aste-mallit muistissa ja pysyvästä storagesta tarvittaessa."""
    global _theta_by_ctx, _P_by_ctx, _params_loaded

    if _params_loaded:
        return

    _theta_by_ctx = {}
    _P_by_ctx = {}

    _load_params_from_store()

    # Alusta P jokaiselle olemassa olevalle kontekstille
    for key in _theta_by_ctx.keys():
        _P_by_ctx[str(key)] = [[P0, 0, 0, 0],
                               [0, P0, 0, 0],
                               [0, 0, P0, 0],
                               [0, 0, 0, P0]]

    _params_loaded = True

# ---------- Matriisiapuja ----------
def _dot(a, b):
    total = 0.0
    for i in range(len(a)):
        total += a[i] * b[i]
    return total

def _matvec(M, v):
    out = [0.0, 0.0, 0.0, 0.0]
    for i in range(4):
        s = 0.0
        for j in range(4):
            s += M[i][j] * v[j]
        out[i] = s
    return out

def _matsub(A, B):
    C = [[0.0] * 4 for _ in range(4)]
    for i in range(4):
        for j in range(4):
            C[i][j] = A[i][j] - B[i][j]
    return C

def _matscale(A, s):
    C = [[0.0] * 4 for _ in range(4)]
    for i in range(4):
        for j in range(4):
            C[i][j] = s * A[i][j]
    return C

def _rls_update(theta, P, x, y, lam=LAMBDA):
    Px = _matvec(P, x)
    denom = lam + _dot(x, Px)
    if denom == 0:
        denom = 1e-6
    K = [v / denom for v in Px]
    err_est = y - _dot(x, theta)
    theta = [theta[0] + K[0] * err_est,
             theta[1] + K[1] * err_est,
             theta[2] + K[2] * err_est,
             theta[3] + K[3] * err_est]
    xTP = [[0.0] * 4 for _ in range(4)]
    for i in range(4):
        for j in range(4):
            xTP[i][j] = x[i] * Px[j]
    P = _matscale(_matsub(P, xTP), 1.0 / lam)
    return theta, P

# ---------- Sääennuste ----------
def _avg_future_outdoor():
    attrs = state.getattr(WEATHER) or {}
    fc = attrs.get("forecast") or []
    temps = []
    idx = 0
    for f in fc:
        if idx >= FORECAST_H:
            break
        t = f.get("temperature")
        if t is not None:
            try:
                temps.append(float(t))
                idx += 1
            except Exception:
                pass
    if len(temps) > 0:
        s = 0.0
        for v in temps:
            s += v
        return s / float(len(temps))
    try:
        return float(state.get(OUTDOOR) or 0.0)
    except Exception:
        return 0.0

# ---------- Selectin optiot & pykälöinti ----------
def _with_pct():
    attrs = state.getattr(SELECT) or {}
    opts = attrs.get("options") or []
    return ('%' in (opts[0] if opts else ''))

def _select_options_nums():
    attrs = state.getattr(SELECT) or {}
    opts = attrs.get("options") or []
    nums = []
    for o in opts:
        try:
            s = str(o).replace('%', '').strip()
            nums.append(float(s))
        except Exception:
            pass
    nums.sort()
    has_pct = False
    if len(opts) > 0 and ('%' in str(opts[0])):
        has_pct = True
    return nums, has_pct

def _snap_to_select(value, direction):
    nums, has_pct = _select_options_nums()
    picked = None
    if len(nums) == 0:
        picked = round(value)
        out = str(int(picked)) + ('%' if (has_pct or _with_pct()) else '')
        return out

    if direction > 0:
        chosen = None
        for n in nums:
            if n >= value:
                chosen = n
                break
        if chosen is None:
            chosen = nums[-1]
        picked = chosen
    elif direction < 0:
        chosen = None
        for i in range(len(nums) - 1, -1, -1):
            if nums[i] <= value:
                chosen = nums[i]
                break
        if chosen is None:
            chosen = nums[0]
        picked = chosen
    else:
        best = nums[0]
        bestd = abs(nums[0] - value)
        for n in nums:
            d = abs(n - value)
            if d < bestd:
                bestd = d
                best = n
        picked = best

    if float(picked).is_integer():
        picked = int(picked)
    return str(picked) + ('%' if has_pct else '')

def _clip(v, lo, hi):
    if v < lo:
        return lo
    if v > hi:
        return hi
    return v

# ---------- AUTO-TUNING (vain sisäiseen käyttöön + log.info) ----------
def _auto_tune_helpers(theta, ctx, step_limit_current, deadband_current):
    """
    Laske automaattinen step_limit ja deadband thetan perusteella.
    Käytetään erityisesti theta[2]: demandin vaikutus lämpötilan muutokseen.
    EI kutsu HA-palveluja; käyttää arvoja sisäisesti ja loggaa ehdotuksen.
    """
    try:
        gain = abs(float(theta[2]))
    except Exception:
        gain = 1.0

    if gain < 0.1:
        gain = 0.1
    if gain > 10.0:
        gain = 10.0

    step_raw = AUTO_STEP_BASE / gain
    step_auto = _clip(step_raw, AUTO_STEP_MIN, AUTO_STEP_MAX)

    db_raw = AUTO_DB_BASE * (1.0 + 0.2 * gain)
    db_auto = _clip(db_raw, AUTO_DB_MIN, AUTO_DB_MAX)

    step_auto_rounded = round(step_auto, 1)
    db_auto_rounded   = round(db_auto, 2)

    if (abs(step_auto_rounded - step_limit_current) > 0.1 or
            abs(db_auto_rounded - deadband_current) > 0.01):
        log.info(
            "Daikin ML AUTO-TUNE suggestion (ctx=%s): gain=%.3f -> "
            "step_limit=%.1f (was %.1f), deadband=%.2f (was %.2f)",
            ctx, gain,
            step_auto_rounded, step_limit_current,
            db_auto_rounded,   deadband_current,
        )

    # Palautetaan suoraan autotunetut arvot sisäiseen käyttöön
    return step_auto_rounded, db_auto_rounded

# ---------- Varsinainen ohjain ----------
@state_trigger(f"{INDOOR}", f"{OUTDOOR}", f"{INDOOR_RATE}")
@time_trigger("cron(*/6 * * * *)")
def daikin_ml_controller(**kwargs):
    global _last_defrosting, _cooldown_until, _theta_by_ctx, _P_by_ctx

    _init_context_params_if_needed()

    try:
        Tin = float(state.get(INDOOR))
    except Exception:
        Tin = float("nan")
    try:
        Tout = float(state.get(OUTDOOR))
    except Exception:
        Tout = float("nan")
    try:
        rate = float(state.get(INDOOR_RATE) or 0.0)
    except Exception:
        rate = 0.0

    sp = float(state.get(SP_HELPER) or 22.5)

    step_limit_current = float(state.get(STEP_LIMIT_HELPER) or 10.0)
    deadband_current   = float(state.get(DEADBAND_HELPER) or 0.1)

    prev_str   = state.get(SELECT) or ""
    try:
        prev = float(prev_str.replace('%', ''))
    except Exception:
        prev = 60.0

    try:
        liquid = float(state.get(LIQUID) or 100.0)
    except Exception:
        liquid = 100.0
    defrosting = (liquid < 20.0)

    now = time.time()
    if _last_defrosting is None:
        _last_defrosting = defrosting
    if (_last_defrosting is True) and (defrosting is False):
        _cooldown_until = now + COOLDOWN_MINUTES * 60.0
        log.info("Daikin ML: defrost ended -> cooldown active for %d min", COOLDOWN_MINUTES)
    _last_defrosting = defrosting
    in_cooldown = (now < _cooldown_until)

    if not (isfinite(Tin) and isfinite(Tout)):
        log.info("Daikin ML: sensors not ready; Tin=%s Tout=%s", state.get(INDOOR), state.get(OUTDOOR))
        return

    ctx = _context_key_for_outdoor(Tout)
    if ctx not in _theta_by_ctx:
        _theta_by_ctx[ctx] = [0.0, 0.0, 5.0, 0.0]
    if ctx not in _P_by_ctx:
        _P_by_ctx[ctx] = [[P0, 0, 0, 0],
                          [0, P0, 0, 0],
                          [0, 0, P0, 0],
                          [0, 0, 0, P0]]

    theta = _theta_by_ctx[ctx]
    P     = _P_by_ctx[ctx]

    # Auto-tune käyttöön sisäisesti
    step_limit, deadband = _auto_tune_helpers(theta, ctx, step_limit_current, deadband_current)

    in_icing_band = (Tout >= ICING_BAND_MIN and Tout <= ICING_BAND_MAX)
    band_upper = min(MAX_DEM, (ICING_BAND_CAP if in_icing_band else 100.0))

    if prev > band_upper:
        option_cap = _snap_to_select(band_upper, -1)
        if option_cap and option_cap != prev_str:
            select.select_option(entity_id=SELECT, option=option_cap)
            log.info(
                "Daikin ML: CAP ENFORCED: prev=%s -> %s (upper=%.0f%%, ctx=%s)",
                prev_str, option_cap, band_upper, ctx,
            )
        return

    prev_eff = prev if prev <= band_upper else band_upper

    err         = sp - Tin
    demand_norm = _clip(prev_eff / 100.0, 0.0, 1.0)
    x = [1.0, err, demand_norm, (Tout - Tin)]
    y = rate

    if not defrosting:
        theta, P = _rls_update(theta, P, x, y)
        _theta_by_ctx[ctx] = theta
        _P_by_ctx[ctx] = P
        _save_params_to_store()
    else:
        _theta_by_ctx[ctx] = theta
        _P_by_ctx[ctx] = P

    Tout_future = _avg_future_outdoor()
    Tout_eff    = 0.5 * Tout + 0.5 * Tout_future
    dTin_target = _clip(KAPPA * err / HORIZON_H, -2.0, 2.0)

    num        = dTin_target - (theta[0] + theta[1] * err + theta[3] * (Tout_eff - Tin))
    denom_raw  = theta[2]
    denom_sign = 1.0 if denom_raw >= 0 else -1.0
    denom_mag  = abs(denom_raw)
    if denom_mag < 0.5:
        denom_mag = 0.5
    dem_opt = _clip((num / (denom_mag * denom_sign)) * 100.0, 0.0, 100.0)

    if abs(err) <= deadband:
        dem_target = prev_eff
    else:
        dem_target = dem_opt

    delta = dem_target - prev_eff
    if delta > step_limit:
        dem_target = prev_eff + step_limit
    elif delta < -step_limit:
        dem_target = prev_eff - step_limit

    dem_clip = _clip(dem_target, MIN_DEM, band_upper)

    direction = 0
    if Tin < sp - deadband:
        direction = 1
    elif Tin > sp + deadband:
        direction = -1

    if direction == 1:
        up_limit = COOLDOWN_STEP_UP if in_cooldown else step_limit
        forced   = _clip(prev_eff + up_limit, MIN_DEM, band_upper)
        dem_mono = dem_clip if dem_clip > forced else forced
    elif direction == -1:
        forced   = _clip(prev_eff - step_limit, MIN_DEM, band_upper)
        dem_mono = dem_clip if dem_clip < forced else forced
    else:
        forced   = prev_eff
        dem_mono = prev_eff

    option = _snap_to_select(dem_mono, direction)

    try:
        opt_num = float(str(option).replace('%', ''))
    except Exception:
        opt_num = 999.0
    if opt_num > band_upper:
        option = _snap_to_select(band_upper, -1)
        log.info("Daikin ML: post-cap clamp -> %s (upper=%.0f%%, ctx=%s)", option, band_upper, ctx)

    if option and option != prev_str:
        select.select_option(entity_id=SELECT, option=option)

    theta_str = "[" + ", ".join([str(round(float(v), 4)) for v in theta]) + "]"
    cool_str  = "ACTIVE" if in_cooldown else "off"
    icing_str = "ON" if in_icing_band else "off"
    log.info(
        "Daikin ML: ctx=%s | Tin=%.2f°C, Tout=%.1f°C (→%.1f°C), DEFROST=%s, cooldown=%s, "
        "icing_band=%s(cap=%s), theta=%s | "
        "SP=%.2f, step_limit=%.1f, deadband=%.2f | "
        "prev=%s → opt≈%.0f%% → "
        "clip=%.0f%% → forced=%.0f%% → select=%s",
        ctx, Tin, Tout, Tout_future, str(defrosting), cool_str,
        icing_str, (str(ICING_BAND_CAP) if in_icing_band else "off"),
        theta_str, sp, step_limit, deadband,
        prev_str, dem_opt, dem_clip, forced, option,
    )

# ---------- CAP-VARTIJA ----------
@state_trigger(f"{SELECT}")
def daikin_ml_cap_guard(value=None, **kwargs):
    try:
        Tout = float(state.get(OUTDOOR))
    except Exception:
        return
    in_icing_band = (Tout >= ICING_BAND_MIN and Tout <= ICING_BAND_MAX)
    band_upper = min(MAX_DEM, (ICING_BAND_CAP if in_icing_band else 100.0))

    curr_str = state.get(SELECT) or ""
    try:
        curr = float(str(curr_str).replace('%', ''))
    except Exception:
        return

    if curr > band_upper:
        option_cap = _snap_to_select(band_upper, -1)
        if option_cap and option_cap != curr_str:
            select.select_option(entity_id=SELECT, option=option_cap)
            log.info(
                "Daikin ML: CAP GUARD: %s -> %s (upper=%.0f%%)",
                curr_str, option_cap, band_upper,
            )

# ---------- Käsin ajettava askel ----------
@service
def daikin_ml_step():
    """Aja ohjain kerran (voi kutsua HA-automaatioista)."""
    try:
        daikin_ml_controller()
    except Exception as e:
        log.error("Daikin ML step error: %s", e)

DC Guard + DC Ohjaus:

YAML:
- id: daikin_demand_control_p_controller_select
  alias: Daikin demand control - P-säädin 22.5°C (select)
  triggers:
  - entity_id: sensor.living_room_lampotila
    trigger: state
  - trigger: state
    entity_id:
    - sensor.iv_tulo_lampotila
  - trigger: state
    entity_id:
    - sensor.sisalampotila_aula
  - event: start
    trigger: homeassistant
  - trigger: time_pattern
    minutes: /5
    enabled: false
  conditions: []
  actions:
  - variables:
      setpoint: '{{ states(''input_number.daikin_setpoint'') | float(22.5) }}'
      kp: '{{ states(''input_number.daikin_kp'') | float(12) }}'
      kd: '{{ states(''input_number.daikin_kd'') | float(20) }}'
      base: '{{ states(''input_number.daikin_base'') | float(60) }}'
      load_gain: '{{ states(''input_number.daikin_load_gain'') | float(2.0) }}'
      fc_w: '{{ states(''input_number.daikin_forecast_weight'') | float(0.5) }}'
      step_limit: '{{ states(''input_number.daikin_step_limit'') | float(10) }}'
      learn_rate: '{{ states(''input_number.daikin_base_learn_rate'') | float(0.10)
        }}'
      deadband: '{{ states(''input_number.daikin_deadband'') | float(0.1) }}'
      min_dem: 30
      max_dem: 100
      forecast_hours: 6
      with_pct: '{{ ''%'' in (state_attr(''select.faikin_demand_control'',''options'')
        | first | default('''')) }}'
      temp_in: '{{ states(''sensor.living_room_lampotila'') | float(22.5) }}'
      temp_out_now: '{{ states(''sensor.iv_tulo_lampotila'') | float(0) }}'
      rate_in_h: '{{ states(''sensor.sisalampotila_aula'') | float(0) }}'
      liquid: '{{ states(''sensor.faikin_liquid'') | float(100) }}'
      defrosting: '{{ liquid < 20 }}'
      forecast_list: '{{ state_attr(''weather.koti'',''forecast'') or [] }}'
      future_temps: "{% set temps = [] %} {% for f in forecast_list[:forecast_hours]
        %}\n  {% if f.temperature is defined %}\n    {% set _ = temps.append( f.temperature
        | float ) %}\n  {% endif %}\n{% endfor %} {{ temps }}"
      temp_out_future: "{% set tnow = temp_out_now %} {% if future_temps and future_temps|length
        > 0 %}\n  {{ (future_temps | sum / future_temps|length) | float }}\n{% else
        %}\n  {{ tnow }}\n{% endif %}"
      error: '{{ (setpoint - temp_in) | float }}'
      p_term: '{{ kp * error }}'
      d_term: '{{ - kd * rate_in_h }}'
      dT_out_now: '{{ (setpoint - temp_out_now) | float }}'
      dT_out_future: '{{ (setpoint - temp_out_future) | float }}'
      ff_now: '{{ load_gain * dT_out_now }}'
      ff_future: '{{ load_gain * dT_out_future }}'
      ff_mix: '{{ (1 - fc_w) * ff_now + fc_w * ff_future }}'
      prev_str: '{{ states(''select.faikin_demand_control'') }}'
      prev: '{{ (prev_str | replace(''%'','''')) | float(60) }}'
      raw_dem: '{{ base + ff_mix + p_term + d_term }}'
      dem_db: "{% if error | abs <= deadband %}\n  {{ prev }}\n{% else %}\n  {{ raw_dem
        }}\n{% endif %}"
      dem_step: "{% set diff = dem_db - prev %} {% if diff > step_limit %}\n  {{ prev
        + step_limit }}\n{% elif diff < -step_limit %}\n  {{ prev - step_limit }}\n{%
        else %}\n  {{ dem_db }}\n{% endif %}"
      dem_clip: '{{ [max_dem, [min_dem, dem_step] | max] | min }}'
      dir: '{% if temp_in < setpoint - deadband %} 1 {% elif temp_in > setpoint +
        deadband %} -1 {% else %} 0 {% endif %}'
      forced_step: "{% if dir == 1 %}\n  {{ [prev + step_limit, max_dem] | min }}\n{%
        elif dir == -1 %}\n  {{ [prev - step_limit, min_dem] | max }}\n{% else %}\n
        \ {{ prev }}\n{% endif %}"
      dem_mono: "{% if dir == 1 %}\n  {{ [dem_clip, forced_step] | max }}\n{% elif
        dir == -1 %}\n  {{ [dem_clip, forced_step] | min }}\n{% else %}\n  {{ prev
        }}\n{% endif %}"
      target_option: "{% set opts = state_attr('select.faikin_demand_control','options')
        or [] %} {% set nums = (opts | map('replace','%','') | map('float') | list
        | sort) %} {% if not nums %}\n  {{ (dem_mono | round(0)) ~ ( '%' if with_pct
        else '' ) }}\n{% elif dir == 1 %}\n  {% set pick = (nums | select('ge', dem_mono)
        | list | first) or (nums | last) %}\n  {{ (pick | int if pick % 1 == 0 else
        pick) ~ ( '%' if with_pct else '' ) }}\n{% elif dir == -1 %}\n  {% set pick
        = (nums | select('le', dem_mono) | list | last) or (nums | first) %}\n  {{
        (pick | int if pick % 1 == 0 else pick) ~ ( '%' if with_pct else '' ) }}\n{%
        else %}\n  {{ prev_str }}\n{% endif %}"
      should_learn: "{{ dir == 0\n   and learn_rate > 0\n   and (prev >= (min_dem
        + 5))\n   and (prev <= (max_dem - 5))\n   and (not defrosting) }}"
      base_new_raw: '{{ (1 - learn_rate) * base + learn_rate * prev }}'
      base_new: '{{ [max_dem, [min_dem, base_new_raw] | max] | min | round(0) }}'
  - choose:
    - conditions:
      - condition: template
        value_template: '{{ target_option != prev_str }}'
      sequence:
      - target:
          entity_id: select.faikin_demand_control
        data:
          option: '{{ target_option }}'
        action: select.select_option
  - choose:
    - conditions:
      - condition: template
        value_template: '{{ should_learn and ((base_new - base) | abs >= 1) }}'
      sequence:
      - target:
          entity_id: input_number.daikin_base
        data:
          value: '{{ base_new }}'
        action: input_number.set_value
  - data:
      name: Daikin PD+FF (monotone+learn, defrost-aware)
      message: Tin={{ temp_in }}°C, Tout={{ temp_out_now }}°C (→{{ temp_out_future
        }}°C), err={{ error|round(2) }}, rate={{ rate_in_h|round(2) }}°C/h, FF={{
        ff_mix|round(1) }}, Defrost={{ defrosting }} (liquid={{ liquid }}), Icing={{
        'ON' if (temp_out_now >= -4 and temp_out_now <= 4) else 'off' }}(cap=80),
        Prev={{ prev|round(0) }} → Raw={{ raw_dem|round(0) }} → Step={{ dem_step|round(0)
        }} → Clip={{ dem_clip|round(0) }} → Mono={{ dem_mono|round(0) }} → Select={{
        target_option }}
      entity_id: select.faikin_demand_control
    action: logbook.log
- id: daikin_cap_guard
  alias: Daikin ML – Cap guard (icing band)
  initial_state: true
  mode: restart
  trigger:
  - platform: state
    entity_id: select.faikin_demand_control
  variables:
    temp_out_now: '{{ states(''sensor.iv_tulo_lampotila'') | float(999) }}'
    icing_on: '{{ -4.0 <= temp_out_now <= 4.0 }}'
    band_upper: '{{ 80 if icing_on else 100 }}'
    curr_str: '{{ trigger.to_state.state }}'
    curr: '{{ curr_str | replace(''%'','''') | float(0) }}'
    opts: '{{ state_attr(''select.faikin_demand_control'',''options'') or [] }}'
    has_pct: '{{ ''%'' in (opts[0] if opts|length>0 else '''') }}'
    option_cap: '{{ (band_upper | round(0)) ~ (''%'' if has_pct else '''') }}'
  condition:
  - condition: template
    value_template: '{{ curr > band_upper }}'
  action:
  - service: select.select_option
    target:
      entity_id: select.faikin_demand_control
    data:
      option: '{{ option_cap }}'
  - service: logbook.log
    data:
      name: Daikin ML
      message: 'CAP GUARD: {{ curr_str }} -> {{ option_cap }} (upper={{ band_upper|round(0)
        }}%, icing={{ ''ON'' if icing_on else ''off'' }})'
      entity_id: select.faikin_demand_control
 
Viimeksi muokattu:

fhat

Jäsen
Onko joku asettanut faikinia näihin nepuramallin pumppuihin?

Millainen mahdollisuus tuon faikinin laittamisessa on, että rikkoo jotain?
 
Back
Ylös Bottom