Is de Homewizard batterij iets voor mij?

Virtuele thuisbatterij simuleren in Homey — met Claude als bouwer

Meet met je eigen P1-data of een thuisbatterij zich terugverdient, zonder een batterij te kopen.


Wat dit is

Een Advanced Flow die elke minuut je P1-meterstand uitleest en doet alsof je een thuisbatterij hebt. Hij simuleert laden bij teruglevering, ontladen bij afname, houdt de laadtoestand bij en telt op wat je bespaard zou hebben. Na een paar weken heb je een cijfermatig antwoord op “is een thuisbatterij iets voor mij” — gebaseerd op jouw werkelijke verbruikspatroon, niet op een rekentool met aannames.

Waarom nu relevant: met het einde van de saldering per 1-1-2027 verschuift de businesscase van batterijen aanzienlijk. Een simulatie die maanden meeloopt geeft je een veel harder antwoord dan een schatting.

Belangrijk vooraf: dit is een simulatie. Er wordt niets aangestuurd, er verandert niets aan je installatie. De flow schrijft alleen naar logische variabelen.


Wat je nodig hebt

  • Homey Pro (getest op firmware 13.x)
  • Een P1-meter in Homey met een measure_power capability (bijv. HomeWizard)
  • Claude met de Homey MCP-koppeling (https://mcp.athom.com, via OAuth)
  • Optioneel: een dynamisch energiecontract voor realistische prijsberekening

Hoe het werkt

Elke minuut:

P1 uitlezen
   │
   ├── P1 > 0 (je neemt af van het net)
   │      └── batterij zou ontladen
   │             ├── gevraagd vermogen > max ontlaadvermogen?  → begrens op max
   │             └── genoeg energie in de batterij?            → anders lever wat er is
   │
   └── P1 < 0 (je levert terug)
          └── batterij zou laden
                 ├── teruglevering > max laadvermogen?  → begrens op max
                 └── past het nog in de batterij?       → anders vul tot vol

Daarna, ongeacht welke tak:
   Evirtual += Pvirtual / 60          (cumulatieve verschoven energie)
   ROI      += Prijs × Pvirtual/60/1000   (cumulatieve besparing in €)

Acht eindpunten, één gedeelde afsluiting. In totaal 27 kaarten.

Ontwerpkeuze: in veel bestaande schema’s staat de ROI-berekening acht keer herhaald, één keer per tak. Dat is wiskundig identiek aan één gedeelde berekening aan het eind, omdat elke tak Pvirtual al correct heeft gezet. Eén berekening in plaats van acht scheelt kaarten én foutkansen.


Stap 1 — Variabelen aanmaken

Maak deze aan via de knop Variabelen in de flow-editor, allemaal type Nummer.

Naam Waarde Betekenis
Batt2-E 0 Huidige energie-inhoud (Wh)
Batt2-Emax 10000 Capaciteit (Wh)
Batt2-Pmax-charge 7680 Max laadvermogen (W)
Batt2-Pmax-discharge 7680 Max ontlaadvermogen (W)
Batt2-Pin 0 Werkvariabele: P1 van deze minuut
Batt2-Pvirtual 0 Werkvariabele: vermogen deze minuut
Batt2-Evirtual 0 Cumulatieve verschoven energie (Wh)
Batt2-Prijs 0.25 €/kWh
Batt2-ROI 0 Cumulatieve besparing (€)

De waarden hierboven zijn voor 2× Enphase IQ Battery 5P (10 kWh / 7,68 kW). Vervang ze door de specs van de batterij die jij overweegt — dat is het enige dat je hoeft aan te passen om een ander model te simuleren.


Stap 2 — Laat Claude de flow bouwen

Gebruik de prompt onderaan dit bericht. Die bevat alle geverifieerde JSON-formaten, zodat Claude niet opnieuw hoeft uit te zoeken wat wij al hebben getest.


Stap 3 — Verifiëren vóór je 'm laat lopen

Zet Batt2-E tijdelijk op bijvoorbeeld 5000, schakel de flow in en wacht een paar minuten.

Bij afname van het net (P1 positief) hoort Batt2-E te dalen met ongeveer P1 / 60 Wh per minuut. Bij 900 W afname dus zo’n 15 Wh per minuut. Zie je dat, dan werkt de ontlaadtak.

De laadtak zie je pas als je zon hebt en daadwerkelijk terugleveert.

Zet daarna Batt2-E terug op 0 en laat 'm lopen.


Stap 4 — Resultaten bekijken

Alle variabelen verschijnen automatisch in Inzichten onder het kopje Logic, met historie en grafieken.

Kijk naar Wat het je vertelt
Batt2-ROI Cumulatieve besparing in €. Extrapoleer naar een jaar en zet af tegen de aanschafprijs
Batt2-E Vult hij overdag en leegt hij 's avonds? Dan is de capaciteit goed gekozen. Blijft hij vol staan → te groot. Loopt hij continu leeg → te klein
Batt2-Evirtual Totaal verschoven volume in Wh

Reken op minimaal een week voor een trend, en liefst een volle maand met wisselend weer.


De technische lessen — dit is waar we uren aan kwijt waren

Het bouwen van Advanced Flows via de Homey API loopt vast op tokenformaten die nergens gedocumenteerd staan. Het vervelende is dat een fout formaat geen foutmelding geeft — de kaart wordt geaccepteerd, broken blijft false, en de actie doet gewoon stilletjes niets. Dit zijn de formaten die wij empirisch hebben geverifieerd.

Variabele-token

[[homey:manager:logic|<variabele-UUID>]]

Let op: het volledige interne UUID, niet de variabelenaam. [[Batt2-E]] werkt niet. Het UUID haal je op met de MCP-tool search_flow_card_autocomplete_argument (argumentId variable).

Apparaat-token

[[homey:device:<device-UUID>|<capability>]]

Pijpteken tussen device en capability, geen dubbele punt. Bijvoorbeeld: [[homey:device:79e04a09-...|measure_power]]

Berekening

Kaart homey:manager:logic:variable_set_number_math:

json

{
  "type": "action",
  "id": "homey:manager:logic:variable_set_number_math",
  "args": {
    "variable": { "id": "<UUID>", "name": "Batt2-E" },
    "value": "{{ [[homey:manager:logic|<UUID>]] - 100 }}"
  }
}

Het token moet binnen de accolades staan. Dit was de valkuil die ons het meeste tijd kostte: een token buiten de accolades wordt geaccepteerd, maar levert onzin op.

Het variable-argument moet het volledige object {id, name} zijn, niet alleen de naam.

Vertakking op variabelen — zonder droptoken

De standaard vergelijkingskaarten (logic:lt, logic:gt) gebruiken een droptoken-veld dat via de API onbetrouwbaar is. Gebruik in plaats daarvan:

json

{
  "type": "condition",
  "id": "homey:manager:logic:equation",
  "args": {
    "value1": "{{ [[homey:manager:logic|<UUID-A>]] }}",
    "operator": "lte",
    "value2": "{{ [[homey:manager:logic|<UUID-B>]] }}"
  },
  "outputTrue": ["<kaart-id>"],
  "outputFalse": ["<kaart-id>"]
}

Beide waardevelden zijn gewoon tekst en accepteren dezelfde tokensyntax. Operators: eq, neq, lt, gt, lte, gte. Hiermee kun je variabelen onderling vergelijken én rekenen in de vergelijking zelf ({{ [[token]] / 60 }}).

Dit omzeilt de droptoken-beperking volledig en is de sleutel tot complexe flows via de API.

Vertraging

json

{ "type": "delay", "args": { "delay": { "number": "10", "multiplier": 60 } } }

multiplier 60 = minuten, 1 = seconden. number is een string.

Trigger elke minuut

json

{ "id": "homey:manager:cron:every_nth", "args": { "n": 1, "type": "minute" } }

Overige API-eigenaardigheden

  • Kaartsleutels in de JSON moeten geldige UUID’s zijn
  • Er is geen delete_flow tool — opruimen doe je handmatig in de app
  • start_flow werkt alleen op flows met een handmatige start-trigger, niet op cron-triggers
  • search_flow_card_autocomplete_argument werkt wél voor gewone autocomplete-argumenten, maar geeft Not Implemented voor droptoken

Methodische les — hoe je dit soort dingen uitzoekt

Omdat fouten stil zijn, werkt seriële trial-and-error slecht. Wat wél werkte:

  1. Zet elke testvariabele eerst op een ‘vuile’ niet-nulwaarde (bijv. 999). Anders kun je “de actie deed niets” niet onderscheiden van “de actie zette hem op 0”.
  2. Test meerdere hypotheses tegelijk, elk op een eigen doelvariabele. In één run zie je meteen welke werkt.
  3. Neem altijd een controletest mee zonder de verdachte constructie — bijvoorbeeld {{ 3 * 2 + 6 }} zonder token. Werkt die niet, dan ligt het niet aan je tokenformaat maar aan iets fundamentelers.
  4. Bouw één kaart handmatig in de app en lees de JSON terug. Het juiste formaat staat er dan letterlijk in. Dit gaf ons uiteindelijk de doorbraak.

Beperkingen — wees hier eerlijk over

Momentopname, geen gemiddelde. De flow leest P1 één keer per minuut. Een korte piek precies op het meetmoment vertekent die minuut. Over duizenden metingen middelt dat grotendeels uit, maar het is minder nauwkeurig dan een echt minuutgemiddelde. Wie het preciezer wil: bouw een accumulator die elke 10 seconden optelt en per minuut deelt.

Rendement niet gemodelleerd. Een echte batterij heeft round-trip verlies (bij LFP zo’n 90–96%). Deze simulatie gaat uit van 100%. De werkelijke opbrengst ligt dus iets lager dan wat je meet.

Geen degradatie, geen DoD-beperking. De simulatie gebruikt de volledige capaciteit en houdt geen rekening met veroudering.

Prijs is een momentopname. Als je de prijskoppeling niet werkend krijgt (zie hieronder), rekent hij met een vaste prijs. Dat onderschat juist de arbitragewaarde die bij een dynamisch contract het interessantst is.

Saldering vertekent de uitkomst. Zolang saldering geldt, wordt teruglevering al vol verrekend. De gemeten ROI is daarmee vooral een indicatie voor de situatie na 2027.


Nog openstaand: prijskoppeling aan een dynamisch contract

Het idee: een aparte flow met als trigger “Stroomprijs verandert” van je energieleverancier-app, die het prijs-token wegschrijft naar Batt2-Prijs. De hoofdsimulatie leest die variabele.

Wij hebben het tokenformaat voor app-triggertokens nog niet geverifieerd. Device- en variabele-tokens wél (zie hierboven), maar een trigger-token uit een app werkt mogelijk anders. Bouw die ene kaart voor de zekerheid handmatig in de app met de token-picker.

Zonder werkende koppeling zet je Batt2-Prijs gewoon handmatig op een realistisch gemiddelde. De simulatie werkt dan prima, alleen minder scherp.


De prompt

Plak dit in Claude, met de Homey MCP-koppeling actief.

Ik wil in Homey een virtuele thuisbatterij-simulatie bouwen via de Homey MCP.
Doel: elke minuut mijn P1-meting uitlezen en simuleren wat een thuisbatterij
zou hebben gedaan, zodat ik na een paar weken kan zien of aanschaf rendeert.

Er wordt niets aangestuurd — alleen logische variabelen bijwerken.

=== BATTERIJSPECS (pas aan naar wens) ===
Capaciteit: 10000 Wh
Max laadvermogen: 7680 W
Max ontlaadvermogen: 7680 W

=== VARIABELEN (ik heb ze al aangemaakt, type Nummer) ===
Batt2-E (0), Batt2-Emax (10000), Batt2-Pmax-charge (7680),
Batt2-Pmax-discharge (7680), Batt2-Pin (0), Batt2-Pvirtual (0),
Batt2-Evirtual (0), Batt2-Prijs (0.25), Batt2-ROI (0)

Haal hun UUID's op met search_flow_card_autocomplete_argument
(cardId: homey:manager:logic:variable_set_number_math, argumentId: variable).
Zoek ook mijn P1-device en de bijbehorende measure_power capability op.

=== GEVERIFIEERDE JSON-FORMATEN — gebruik deze, ga niet zelf experimenteren ===

Variabele-token:  [[homey:manager:logic|<variabele-UUID>]]
                  (volledig UUID, NIET de naam)
Apparaat-token:   [[homey:device:<device-UUID>|<capability>]]
                  (pijpteken, geen dubbele punt)

Berekening:
{"type":"action","id":"homey:manager:logic:variable_set_number_math",
 "args":{"variable":{"id":"<UUID>","name":"<naam>"},
         "value":"{{ [[homey:manager:logic|<UUID>]] - 100 }}"}}
→ token MOET binnen de accolades staan
→ variable MOET het volledige object {id, name} zijn

Vaste waarde:
{"type":"action","id":"homey:manager:logic:variable_set_number",
 "args":{"variable":{"id":"<UUID>","name":"<naam>"},"value":0}}

Vertakking (gebruik deze, NIET logic:lt of logic:gt met droptoken —
die zijn via de API onbetrouwbaar):
{"type":"condition","id":"homey:manager:logic:equation",
 "args":{"value1":"{{ [[token]] }}","operator":"lte","value2":"{{ [[token]] }}"},
 "outputTrue":["<id>"],"outputFalse":["<id>"]}
→ operators: eq, neq, lt, gt, lte, gte
→ beide velden zijn tekst en mogen rekenen: "{{ [[token]] / 60 }}"

Trigger elke minuut:
{"type":"trigger","id":"homey:manager:cron:every_nth",
 "args":{"n":1,"type":"minute"}}

Kaartsleutels in de JSON moeten geldige UUID's zijn.

=== TE BOUWEN FLOW ===

Naam: "Batt2 Simulatie (virtuele batterij)", enabled: false

Trigger elke minuut
→ Batt2-Pin = P1 measure_power
→ conditie: Batt2-Pin > 0 ?

ONTLADEN (waar):
  conditie: Pin > Pmax-discharge ?
    waar:  conditie: Pmax-discharge/60 <= E ?
             waar:  Pvirtual = Pmax-discharge ; E = E - Pmax-discharge/60
             onwaar: Pvirtual = E * 60         ; E = 0
    onwaar: conditie: Pin/60 <= E ?
             waar:  Pvirtual = Pin  ; E = E - Pin/60
             onwaar: Pvirtual = E*60 ; E = 0

LADEN (onwaar):
  conditie: -1*Pin <= Pmax-charge ?
    waar:  conditie: -1*Pin/60 <= (Emax - E) ?
             waar:  Pvirtual = Pin              ; E = E - Pin/60
             onwaar: Pvirtual = -1*(Emax-E)*60  ; E = Emax
    onwaar: conditie: Pmax-charge/60 <= (Emax - E) ?
             waar:  Pvirtual = -1*Pmax-charge   ; E = E + Pmax-charge/60
             onwaar: Pvirtual = -1*(Emax-E)*60  ; E = Emax

Alle acht eindpunten komen samen op:
→ Batt2-Evirtual = Batt2-Evirtual + Batt2-Pvirtual/60
→ Batt2-ROI = Batt2-ROI + Batt2-Prijs * Batt2-Pvirtual/60/1000

BELANGRIJK: zet in elke tak eerst Pvirtual, dán pas E.
Sommige Pvirtual-formules gebruiken de oude waarde van E.

=== DAARNA ===
Bouw ook een losse flow "Batt2 Reset" met een handmatige start-trigger die
E, Pvirtual, Evirtual, Pin en ROI op 0 zet. Handig bij het opnieuw beginnen.

Laat de simulatie uitgeschakeld — ik zet 'm zelf aan na controle.

Waarschuw me als iets niet geverifieerd is in plaats van te gokken.

Tot slot

Als je dit gebruikt: de simulatie is zo goed als je aannames. Kijk vooral naar het patroon van Batt2-E over de dag — dat vertelt je meer over de juiste batterijgrootte dan het ROI-getal alleen. En vergeet niet dat de uitkomst onder saldering anders is dan erna.

Vragen of verbeteringen: reageer gerust. De tokenformaten hierboven zijn empirisch getest, maar Homey-firmware verandert.

Hij draait nu. Wil uitvogelen of een batterij voor mij een uitkomst is of dat ik voorlopig mijn auto aan het net hang met 100% groen laden om het overschot van 41 panelen O-Z-W op te vangen.

Update 1 — virtuele batterij draait, en het derde tokenformaat is gevonden

Vervolg op mijn eerdere bericht over het simuleren van een thuisbatterij in Homey.


Het openstaande punt is opgelost

In het eerste bericht stond de prijskoppeling nog als onopgelost gemarkeerd: het lukte niet om het prijs-token van een energieleverancier-app via de API in een flow te krijgen. Dat is nu gelukt, en het antwoord was verrassend.

Ik had twee formaten geprobeerd die logisch leken op basis van wat al werkte:

[[homey:app:nl.frank-energie:energy_price_changed|price]]   → faalt stil
[[<trigger-kaart-uuid>|price]]                              → faalt stil

Het werkelijke formaat blijkt een derde conventie te volgen, niet af te leiden uit de andere twee:

[[trigger::<trigger-kaart-UUID-in-deze-flow>::price]]

Dus voluit in een rekenkaart:

json

{
  "type": "action",
  "id": "homey:manager:logic:variable_set_number_math",
  "args": {
    "variable": { "id": "<uuid>", "name": "Batt2-Prijs" },
    "value": "{{ [[trigger::21eae21c-9d1b-4473-9d57-03ca16782896::price]] }}"
  }
}

Let op drie dingen:

  • Dubbele dubbelepunt als scheidingsteken, niet het pijpteken dat device- en variabele-tokens gebruiken
  • Voorvoegsel trigger::
  • Het verwijst naar de kaart-instantie — het UUID dat jij zelf aan de triggerkaart in deze flow hebt gegeven, niet naar het kaarttype-ID van de app

Dat laatste betekent dat je dit token alleen kunt gebruiken in dezelfde flow als de bijbehorende trigger. Logisch als je erover nadenkt, maar het maakt het formaat wel onmogelijk te raden.

De drie formaten op een rij

Voor wie flows via de API bouwt, dit is nu de complete set:

Bron Formaat
Logische variabele `[[homey:manager:logic
Apparaat-capability `[[homey:device:
Trigger-token [[trigger::<kaart-uuid>::<token-naam>]]

Drie verschillende conventies voor wat conceptueel hetzelfde is. Alle drie geverifieerd door ze daadwerkelijk te laten draaien en het resultaat te controleren, niet door te redeneren over wat logisch zou zijn.

Hoe je zoiets zelf vindt

De methode die uiteindelijk werkte, en die ik ook voor de andere twee formaten heb gebruikt:

  1. Bouw één kaart handmatig in de Homey-app, met de token-picker
  2. Sla op — en let op: de API geeft de oude versie terug tot je echt op Opslaan hebt geklikt
  3. Lees de flow terug via de API en kijk letterlijk wat er in het JSON-veld staat

Klinkt simpel, maar ik heb er eerst een uur op zitten gokken. De verleiding is groot om te redeneren “device-tokens gebruiken een pijpteken, dus trigger-tokens vast ook” — en dat is precies hoe je erin loopt.

Eén valkuil die ik twee keer heb gemaakt: het token moet binnen de accolades staan. Staat het ervoor, dan wordt de kaart geaccepteerd, blijft broken op false, en levert de berekening onzin op zonder enige foutmelding.

De simulatie draait

Sinds gisteravond loopt hij. Wat ik kan bevestigen uit de Insights-data:

De ontlaadtak klopt. Met de batterij op 5000 Wh als test daalde de inhoud met precies P1 / 60 Wh per minuut:

Batt2-E Afname Impliceert P1
4984,87 −15,13 908 W
4969,75 −15,12 907 W
4954,93 −14,82 889 W
4925,50 −14,63 878 W

Precies wat de formule voorschrijft. De hele keten — apparaat uitlezen, vertakken, rekenen, wegschrijven — werkt in productie.

Insights werkt vanzelf. Alle variabelen verschijnen automatisch onder Logic in Inzichten, met historie en grafieken. Geen extra configuratie nodig.

Wat je moet weten voor je begint

Twee dingen die ik zelf onderschatte:

Begin niet 's avonds. Ik heb de simulatie om half tien 's avonds op nul gezet. Gevolg: de hele nacht gebeurt er niets. De batterij is leeg (dus niets te ontladen) en er is geen zon (dus niets te laden). Twaalf uur aan lege data. Start liever 's ochtends, dan zie je meteen de eerste laadcyclus.

De laadtak is nog niet in productie bewezen. Ontladen wel, laden nog niet — daarvoor moet ik eerst een dag met teruglevering meemaken. Ik meld het als het zover is.

Nog open: mogelijke gemiste cycli

Tijdens de eerste testronde zag ik twee van de acht minuten geen update. Dat zou betekenen dat de cron-trigger niet altijd vuurt, en dat de simulatie systematisch te weinig energie meetelt.

Ik weet nog niet of dit incidenteel was of structureel — twee waarnemingen op acht is te weinig om iets over te zeggen. Zodra er een dag met beweging in de data zit, tel ik het aantal werkelijke updates tegen de verwachte 1440 per etmaal.

Is het structureel, dan is de oplossing waarschijnlijk om de werkelijk verstreken tijd mee te rekenen in plaats van vast te delen door 60. Ik kom erop terug.

Aanvulling op de prompt

Als je de prompt uit het eerste bericht gebruikt en ook prijskoppeling wilt, voeg dit toe aan het blok met geverifieerde formaten:

Trigger-token (bv. prijs uit een energieleverancier-app):
  [[trigger::<trigger-kaart-UUID-in-deze-flow>::<token-naam>]]
  → dubbele dubbelepunt, prefix trigger::
  → verwijst naar de kaart-instantie in deze flow, niet naar het kaarttype
  → werkt alleen in dezelfde flow als de betreffende trigger

Bouw hiervoor een aparte flow:
  Trigger "Stroomprijs verandert" → Bereken Batt2-Prijs als {{ [[trigger::...::price]] }}

Volgende update zodra er een volledige dag met zon doorheen is. Dan weet ik of de laadtak klopt en kan ik de eerste echte laad-/ontlaadcyclus laten zien.

Tip van een zeer tevreden gebruiker: