[{"data":1,"prerenderedAt":3629},["ShallowReactive",2],{"blog-/blog/solar-monitoring-part-2-the-typescript-rebuild":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"updated":10,"tags":11,"series":18,"seriesPart":19,"readingTime":20,"cover":7,"body":21,"_type":3623,"_id":3624,"_source":3625,"_file":3626,"_stem":3627,"_extension":3628},"/blog/solar-monitoring-part-2-the-typescript-rebuild","blog",false,"","DIY Solar Monitoring Rebuilt: TypeScript, MQTT, and Home Assistant","After a PSU cable mistake took out my solar monitoring stack, I rebuilt it with TypeScript, MQTT, and Home Assistant — and it's better than the original.","2026-03-09",[12,13,14,15,16,17],"solar-tracker","homelab","typescript","mqtt","home-assistant","timescaledb","Solar Monitoring Stack",2,"9 min read",{"type":22,"children":23,"toc":3610},"root",[24,41,45,50,63,77,84,89,94,100,120,403,408,999,1004,1011,1016,1611,1616,1622,1627,1632,1640,1645,1651,1656,1661,2112,2117,2130,2136,2149,2154,2784,2789,2801,2807,2820,2825,2960,2973,2978,2984,2989,3477,3490,3503,3509,3520,3537,3555,3565,3571,3576,3581,3586,3591,3594,3599,3604],{"type":25,"tag":26,"props":27,"children":28},"element","p",{},[29,32,39],{"type":30,"value":31},"text","This is part two of a two-part series. ",{"type":25,"tag":33,"props":34,"children":36},"a",{"href":35},"/blog/solar-monitoring-part-1-the-python-build",[37],{"type":30,"value":38},"Part one covers the original build",{"type":30,"value":40}," and how it ended. This is the story of what came next.",{"type":25,"tag":42,"props":43,"children":44},"hr",{},[],{"type":25,"tag":26,"props":46,"children":47},{},[48],{"type":30,"value":49},"Losing your own work stings. But there's a particular flavor of losing work that only hits when you realize the thing you rebuilt is actually better than the thing you lost.",{"type":25,"tag":26,"props":51,"children":52},{},[53,55,61],{"type":30,"value":54},"After a ",{"type":25,"tag":33,"props":56,"children":58},{"href":57},"/blog/well-i-embarrassed-myself-even-sooner-than-expected-a-modular-psu-cables-tale",[59],{"type":30,"value":60},"PSU cable mix-up",{"type":30,"value":62}," took out the drives in my server, I lost all the custom code for my solar monitoring stack. The Python poller, the TypeScript API, the hand-built dashboard — gone. I had two choices: rebuild the same stack, or rethink it.",{"type":25,"tag":26,"props":64,"children":65},{},[66,68,75],{"type":30,"value":67},"The new stack is TypeScript, Node.js, MQTT, TimescaleDB, and Home Assistant. It runs in Docker Compose, every piece of it fits in a ",{"type":25,"tag":69,"props":70,"children":72},"code",{"className":71},[],[73],{"type":30,"value":74},"git push",{"type":30,"value":76},", and adding a new sensor takes about three lines of code.",{"type":25,"tag":78,"props":79,"children":81},"h2",{"id":80},"what-i-changed-and-why",[82],{"type":30,"value":83},"What I Changed and Why",{"type":25,"tag":26,"props":85,"children":86},{},[87],{"type":30,"value":88},"The original stack wasn't around long enough for the refactoring costs to become truly painful, but there was one thing I already knew I didn't want to carry forward: the custom dashboard. A hand-built web interface is satisfying to build and tedious to keep current. Every new metric meant touching the schema, the API, and the dashboard component. The stack worked, but the dashboard was the part I least wanted to rebuild.",{"type":25,"tag":26,"props":90,"children":91},{},[92],{"type":30,"value":93},"So I didn't. The new approach delegates the dashboard to Home Assistant. Not because Home Assistant is perfect, but because someone else maintains it.",{"type":25,"tag":78,"props":95,"children":97},{"id":96},"the-new-poller-typescript-and-modbus-serial",[98],{"type":30,"value":99},"The New Poller: TypeScript and modbus-serial",{"type":25,"tag":26,"props":101,"children":102},{},[103,105,111,113,118],{"type":30,"value":104},"The poller was rewritten in TypeScript on Node.js 20. I used ",{"type":25,"tag":69,"props":106,"children":108},{"className":107},[],[109],{"type":30,"value":110},"modbus-serial",{"type":30,"value":112}," for the Modbus RTU communication and the ",{"type":25,"tag":69,"props":114,"children":116},{"className":115},[],[117],{"type":30,"value":15},{"type":30,"value":119}," library for publishing. The polling interval stayed at 5 seconds.",{"type":25,"tag":121,"props":122,"children":125},"pre",{"code":123,"language":14,"meta":7,"className":124,"style":7},"import ModbusRTU from \"modbus-serial\";\nimport mqtt from \"mqtt\";\n\nconst client = new ModbusRTU();\nawait client.connectRTUBuffered(\"/dev/ttyUSB0\", {\n  baudRate: 9600,\n  parity: \"none\",\n  stopBits: 1,\n  dataBits: 8,\n});\nclient.setID(1);\nclient.setTimeout(3000);\n","language-typescript shiki shiki-themes github-dark",[126],{"type":25,"tag":69,"props":127,"children":128},{"__ignoreMap":7},[129,163,188,198,234,268,287,305,323,341,350,377],{"type":25,"tag":130,"props":131,"children":134},"span",{"class":132,"line":133},"line",1,[135,141,147,152,158],{"type":25,"tag":130,"props":136,"children":138},{"style":137},"--shiki-default:#F97583",[139],{"type":30,"value":140},"import",{"type":25,"tag":130,"props":142,"children":144},{"style":143},"--shiki-default:#E1E4E8",[145],{"type":30,"value":146}," ModbusRTU ",{"type":25,"tag":130,"props":148,"children":149},{"style":137},[150],{"type":30,"value":151},"from",{"type":25,"tag":130,"props":153,"children":155},{"style":154},"--shiki-default:#9ECBFF",[156],{"type":30,"value":157}," \"modbus-serial\"",{"type":25,"tag":130,"props":159,"children":160},{"style":143},[161],{"type":30,"value":162},";\n",{"type":25,"tag":130,"props":164,"children":165},{"class":132,"line":19},[166,170,175,179,184],{"type":25,"tag":130,"props":167,"children":168},{"style":137},[169],{"type":30,"value":140},{"type":25,"tag":130,"props":171,"children":172},{"style":143},[173],{"type":30,"value":174}," mqtt ",{"type":25,"tag":130,"props":176,"children":177},{"style":137},[178],{"type":30,"value":151},{"type":25,"tag":130,"props":180,"children":181},{"style":154},[182],{"type":30,"value":183}," \"mqtt\"",{"type":25,"tag":130,"props":185,"children":186},{"style":143},[187],{"type":30,"value":162},{"type":25,"tag":130,"props":189,"children":191},{"class":132,"line":190},3,[192],{"type":25,"tag":130,"props":193,"children":195},{"emptyLinePlaceholder":194},true,[196],{"type":30,"value":197},"\n",{"type":25,"tag":130,"props":199,"children":201},{"class":132,"line":200},4,[202,207,213,218,223,229],{"type":25,"tag":130,"props":203,"children":204},{"style":137},[205],{"type":30,"value":206},"const",{"type":25,"tag":130,"props":208,"children":210},{"style":209},"--shiki-default:#79B8FF",[211],{"type":30,"value":212}," client",{"type":25,"tag":130,"props":214,"children":215},{"style":137},[216],{"type":30,"value":217}," =",{"type":25,"tag":130,"props":219,"children":220},{"style":137},[221],{"type":30,"value":222}," new",{"type":25,"tag":130,"props":224,"children":226},{"style":225},"--shiki-default:#B392F0",[227],{"type":30,"value":228}," ModbusRTU",{"type":25,"tag":130,"props":230,"children":231},{"style":143},[232],{"type":30,"value":233},"();\n",{"type":25,"tag":130,"props":235,"children":237},{"class":132,"line":236},5,[238,243,248,253,258,263],{"type":25,"tag":130,"props":239,"children":240},{"style":137},[241],{"type":30,"value":242},"await",{"type":25,"tag":130,"props":244,"children":245},{"style":143},[246],{"type":30,"value":247}," client.",{"type":25,"tag":130,"props":249,"children":250},{"style":225},[251],{"type":30,"value":252},"connectRTUBuffered",{"type":25,"tag":130,"props":254,"children":255},{"style":143},[256],{"type":30,"value":257},"(",{"type":25,"tag":130,"props":259,"children":260},{"style":154},[261],{"type":30,"value":262},"\"/dev/ttyUSB0\"",{"type":25,"tag":130,"props":264,"children":265},{"style":143},[266],{"type":30,"value":267},", {\n",{"type":25,"tag":130,"props":269,"children":271},{"class":132,"line":270},6,[272,277,282],{"type":25,"tag":130,"props":273,"children":274},{"style":143},[275],{"type":30,"value":276},"  baudRate: ",{"type":25,"tag":130,"props":278,"children":279},{"style":209},[280],{"type":30,"value":281},"9600",{"type":25,"tag":130,"props":283,"children":284},{"style":143},[285],{"type":30,"value":286},",\n",{"type":25,"tag":130,"props":288,"children":290},{"class":132,"line":289},7,[291,296,301],{"type":25,"tag":130,"props":292,"children":293},{"style":143},[294],{"type":30,"value":295},"  parity: ",{"type":25,"tag":130,"props":297,"children":298},{"style":154},[299],{"type":30,"value":300},"\"none\"",{"type":25,"tag":130,"props":302,"children":303},{"style":143},[304],{"type":30,"value":286},{"type":25,"tag":130,"props":306,"children":308},{"class":132,"line":307},8,[309,314,319],{"type":25,"tag":130,"props":310,"children":311},{"style":143},[312],{"type":30,"value":313},"  stopBits: ",{"type":25,"tag":130,"props":315,"children":316},{"style":209},[317],{"type":30,"value":318},"1",{"type":25,"tag":130,"props":320,"children":321},{"style":143},[322],{"type":30,"value":286},{"type":25,"tag":130,"props":324,"children":326},{"class":132,"line":325},9,[327,332,337],{"type":25,"tag":130,"props":328,"children":329},{"style":143},[330],{"type":30,"value":331},"  dataBits: ",{"type":25,"tag":130,"props":333,"children":334},{"style":209},[335],{"type":30,"value":336},"8",{"type":25,"tag":130,"props":338,"children":339},{"style":143},[340],{"type":30,"value":286},{"type":25,"tag":130,"props":342,"children":344},{"class":132,"line":343},10,[345],{"type":25,"tag":130,"props":346,"children":347},{"style":143},[348],{"type":30,"value":349},"});\n",{"type":25,"tag":130,"props":351,"children":353},{"class":132,"line":352},11,[354,359,364,368,372],{"type":25,"tag":130,"props":355,"children":356},{"style":143},[357],{"type":30,"value":358},"client.",{"type":25,"tag":130,"props":360,"children":361},{"style":225},[362],{"type":30,"value":363},"setID",{"type":25,"tag":130,"props":365,"children":366},{"style":143},[367],{"type":30,"value":257},{"type":25,"tag":130,"props":369,"children":370},{"style":209},[371],{"type":30,"value":318},{"type":25,"tag":130,"props":373,"children":374},{"style":143},[375],{"type":30,"value":376},");\n",{"type":25,"tag":130,"props":378,"children":380},{"class":132,"line":379},12,[381,385,390,394,399],{"type":25,"tag":130,"props":382,"children":383},{"style":143},[384],{"type":30,"value":358},{"type":25,"tag":130,"props":386,"children":387},{"style":225},[388],{"type":30,"value":389},"setTimeout",{"type":25,"tag":130,"props":391,"children":392},{"style":143},[393],{"type":30,"value":257},{"type":25,"tag":130,"props":395,"children":396},{"style":209},[397],{"type":30,"value":398},"3000",{"type":25,"tag":130,"props":400,"children":401},{"style":143},[402],{"type":30,"value":376},{"type":25,"tag":26,"props":404,"children":405},{},[406],{"type":30,"value":407},"The register layout maps about 30 metrics across two protocol areas. P01 covers battery and PV data (input registers), P02 covers inverter state, grid, and load (a mix of input and holding registers). Same fallback logic as the original, but now with TypeScript types so the register definitions are explicit:",{"type":25,"tag":121,"props":409,"children":411},{"code":410,"language":14,"meta":7,"className":124,"style":7},"type RegisterDef = {\n  address: number;\n  name: string;\n  scale: number;\n  unit: string;\n  deviceClass?: string;\n  stateClass?: string;\n};\n\nconst REGISTERS: RegisterDef[] = [\n  { address: 0x0100, name: \"battery_soc\",     scale: 1,    unit: \"%\",  deviceClass: \"battery\",     stateClass: \"measurement\" },\n  { address: 0x0101, name: \"battery_voltage\",  scale: 0.1,  unit: \"V\",  deviceClass: \"voltage\",     stateClass: \"measurement\" },\n  { address: 0x0102, name: \"battery_current\",  scale: 0.1,  unit: \"A\",  deviceClass: \"current\",     stateClass: \"measurement\" },\n  { address: 0x0107, name: \"pv1_voltage\",      scale: 0.1,  unit: \"V\",  deviceClass: \"voltage\",     stateClass: \"measurement\" },\n  { address: 0x0108, name: \"pv1_current\",      scale: 0.1,  unit: \"A\",  deviceClass: \"current\",     stateClass: \"measurement\" },\n  { address: 0x0109, name: \"pv1_power\",        scale: 1,    unit: \"W\",  deviceClass: \"power\",       stateClass: \"measurement\" },\n  // ...~24 more\n];\n",[412],{"type":25,"tag":69,"props":413,"children":414},{"__ignoreMap":7},[415,437,460,481,501,521,542,562,570,577,612,679,741,801,860,918,980,990],{"type":25,"tag":130,"props":416,"children":417},{"class":132,"line":133},[418,423,428,432],{"type":25,"tag":130,"props":419,"children":420},{"style":137},[421],{"type":30,"value":422},"type",{"type":25,"tag":130,"props":424,"children":425},{"style":225},[426],{"type":30,"value":427}," RegisterDef",{"type":25,"tag":130,"props":429,"children":430},{"style":137},[431],{"type":30,"value":217},{"type":25,"tag":130,"props":433,"children":434},{"style":143},[435],{"type":30,"value":436}," {\n",{"type":25,"tag":130,"props":438,"children":439},{"class":132,"line":19},[440,446,451,456],{"type":25,"tag":130,"props":441,"children":443},{"style":442},"--shiki-default:#FFAB70",[444],{"type":30,"value":445},"  address",{"type":25,"tag":130,"props":447,"children":448},{"style":137},[449],{"type":30,"value":450},":",{"type":25,"tag":130,"props":452,"children":453},{"style":209},[454],{"type":30,"value":455}," number",{"type":25,"tag":130,"props":457,"children":458},{"style":143},[459],{"type":30,"value":162},{"type":25,"tag":130,"props":461,"children":462},{"class":132,"line":190},[463,468,472,477],{"type":25,"tag":130,"props":464,"children":465},{"style":442},[466],{"type":30,"value":467},"  name",{"type":25,"tag":130,"props":469,"children":470},{"style":137},[471],{"type":30,"value":450},{"type":25,"tag":130,"props":473,"children":474},{"style":209},[475],{"type":30,"value":476}," string",{"type":25,"tag":130,"props":478,"children":479},{"style":143},[480],{"type":30,"value":162},{"type":25,"tag":130,"props":482,"children":483},{"class":132,"line":200},[484,489,493,497],{"type":25,"tag":130,"props":485,"children":486},{"style":442},[487],{"type":30,"value":488},"  scale",{"type":25,"tag":130,"props":490,"children":491},{"style":137},[492],{"type":30,"value":450},{"type":25,"tag":130,"props":494,"children":495},{"style":209},[496],{"type":30,"value":455},{"type":25,"tag":130,"props":498,"children":499},{"style":143},[500],{"type":30,"value":162},{"type":25,"tag":130,"props":502,"children":503},{"class":132,"line":236},[504,509,513,517],{"type":25,"tag":130,"props":505,"children":506},{"style":442},[507],{"type":30,"value":508},"  unit",{"type":25,"tag":130,"props":510,"children":511},{"style":137},[512],{"type":30,"value":450},{"type":25,"tag":130,"props":514,"children":515},{"style":209},[516],{"type":30,"value":476},{"type":25,"tag":130,"props":518,"children":519},{"style":143},[520],{"type":30,"value":162},{"type":25,"tag":130,"props":522,"children":523},{"class":132,"line":270},[524,529,534,538],{"type":25,"tag":130,"props":525,"children":526},{"style":442},[527],{"type":30,"value":528},"  deviceClass",{"type":25,"tag":130,"props":530,"children":531},{"style":137},[532],{"type":30,"value":533},"?:",{"type":25,"tag":130,"props":535,"children":536},{"style":209},[537],{"type":30,"value":476},{"type":25,"tag":130,"props":539,"children":540},{"style":143},[541],{"type":30,"value":162},{"type":25,"tag":130,"props":543,"children":544},{"class":132,"line":289},[545,550,554,558],{"type":25,"tag":130,"props":546,"children":547},{"style":442},[548],{"type":30,"value":549},"  stateClass",{"type":25,"tag":130,"props":551,"children":552},{"style":137},[553],{"type":30,"value":533},{"type":25,"tag":130,"props":555,"children":556},{"style":209},[557],{"type":30,"value":476},{"type":25,"tag":130,"props":559,"children":560},{"style":143},[561],{"type":30,"value":162},{"type":25,"tag":130,"props":563,"children":564},{"class":132,"line":307},[565],{"type":25,"tag":130,"props":566,"children":567},{"style":143},[568],{"type":30,"value":569},"};\n",{"type":25,"tag":130,"props":571,"children":572},{"class":132,"line":325},[573],{"type":25,"tag":130,"props":574,"children":575},{"emptyLinePlaceholder":194},[576],{"type":30,"value":197},{"type":25,"tag":130,"props":578,"children":579},{"class":132,"line":343},[580,584,589,593,597,602,607],{"type":25,"tag":130,"props":581,"children":582},{"style":137},[583],{"type":30,"value":206},{"type":25,"tag":130,"props":585,"children":586},{"style":209},[587],{"type":30,"value":588}," REGISTERS",{"type":25,"tag":130,"props":590,"children":591},{"style":137},[592],{"type":30,"value":450},{"type":25,"tag":130,"props":594,"children":595},{"style":225},[596],{"type":30,"value":427},{"type":25,"tag":130,"props":598,"children":599},{"style":143},[600],{"type":30,"value":601},"[] ",{"type":25,"tag":130,"props":603,"children":604},{"style":137},[605],{"type":30,"value":606},"=",{"type":25,"tag":130,"props":608,"children":609},{"style":143},[610],{"type":30,"value":611}," [\n",{"type":25,"tag":130,"props":613,"children":614},{"class":132,"line":352},[615,620,625,630,635,640,644,649,654,659,664,669,674],{"type":25,"tag":130,"props":616,"children":617},{"style":143},[618],{"type":30,"value":619},"  { address: ",{"type":25,"tag":130,"props":621,"children":622},{"style":209},[623],{"type":30,"value":624},"0x0100",{"type":25,"tag":130,"props":626,"children":627},{"style":143},[628],{"type":30,"value":629},", name: ",{"type":25,"tag":130,"props":631,"children":632},{"style":154},[633],{"type":30,"value":634},"\"battery_soc\"",{"type":25,"tag":130,"props":636,"children":637},{"style":143},[638],{"type":30,"value":639},",     scale: ",{"type":25,"tag":130,"props":641,"children":642},{"style":209},[643],{"type":30,"value":318},{"type":25,"tag":130,"props":645,"children":646},{"style":143},[647],{"type":30,"value":648},",    unit: ",{"type":25,"tag":130,"props":650,"children":651},{"style":154},[652],{"type":30,"value":653},"\"%\"",{"type":25,"tag":130,"props":655,"children":656},{"style":143},[657],{"type":30,"value":658},",  deviceClass: ",{"type":25,"tag":130,"props":660,"children":661},{"style":154},[662],{"type":30,"value":663},"\"battery\"",{"type":25,"tag":130,"props":665,"children":666},{"style":143},[667],{"type":30,"value":668},",     stateClass: ",{"type":25,"tag":130,"props":670,"children":671},{"style":154},[672],{"type":30,"value":673},"\"measurement\"",{"type":25,"tag":130,"props":675,"children":676},{"style":143},[677],{"type":30,"value":678}," },\n",{"type":25,"tag":130,"props":680,"children":681},{"class":132,"line":379},[682,686,691,695,700,705,710,715,720,724,729,733,737],{"type":25,"tag":130,"props":683,"children":684},{"style":143},[685],{"type":30,"value":619},{"type":25,"tag":130,"props":687,"children":688},{"style":209},[689],{"type":30,"value":690},"0x0101",{"type":25,"tag":130,"props":692,"children":693},{"style":143},[694],{"type":30,"value":629},{"type":25,"tag":130,"props":696,"children":697},{"style":154},[698],{"type":30,"value":699},"\"battery_voltage\"",{"type":25,"tag":130,"props":701,"children":702},{"style":143},[703],{"type":30,"value":704},",  scale: ",{"type":25,"tag":130,"props":706,"children":707},{"style":209},[708],{"type":30,"value":709},"0.1",{"type":25,"tag":130,"props":711,"children":712},{"style":143},[713],{"type":30,"value":714},",  unit: ",{"type":25,"tag":130,"props":716,"children":717},{"style":154},[718],{"type":30,"value":719},"\"V\"",{"type":25,"tag":130,"props":721,"children":722},{"style":143},[723],{"type":30,"value":658},{"type":25,"tag":130,"props":725,"children":726},{"style":154},[727],{"type":30,"value":728},"\"voltage\"",{"type":25,"tag":130,"props":730,"children":731},{"style":143},[732],{"type":30,"value":668},{"type":25,"tag":130,"props":734,"children":735},{"style":154},[736],{"type":30,"value":673},{"type":25,"tag":130,"props":738,"children":739},{"style":143},[740],{"type":30,"value":678},{"type":25,"tag":130,"props":742,"children":744},{"class":132,"line":743},13,[745,749,754,758,763,767,771,775,780,784,789,793,797],{"type":25,"tag":130,"props":746,"children":747},{"style":143},[748],{"type":30,"value":619},{"type":25,"tag":130,"props":750,"children":751},{"style":209},[752],{"type":30,"value":753},"0x0102",{"type":25,"tag":130,"props":755,"children":756},{"style":143},[757],{"type":30,"value":629},{"type":25,"tag":130,"props":759,"children":760},{"style":154},[761],{"type":30,"value":762},"\"battery_current\"",{"type":25,"tag":130,"props":764,"children":765},{"style":143},[766],{"type":30,"value":704},{"type":25,"tag":130,"props":768,"children":769},{"style":209},[770],{"type":30,"value":709},{"type":25,"tag":130,"props":772,"children":773},{"style":143},[774],{"type":30,"value":714},{"type":25,"tag":130,"props":776,"children":777},{"style":154},[778],{"type":30,"value":779},"\"A\"",{"type":25,"tag":130,"props":781,"children":782},{"style":143},[783],{"type":30,"value":658},{"type":25,"tag":130,"props":785,"children":786},{"style":154},[787],{"type":30,"value":788},"\"current\"",{"type":25,"tag":130,"props":790,"children":791},{"style":143},[792],{"type":30,"value":668},{"type":25,"tag":130,"props":794,"children":795},{"style":154},[796],{"type":30,"value":673},{"type":25,"tag":130,"props":798,"children":799},{"style":143},[800],{"type":30,"value":678},{"type":25,"tag":130,"props":802,"children":804},{"class":132,"line":803},14,[805,809,814,818,823,828,832,836,840,844,848,852,856],{"type":25,"tag":130,"props":806,"children":807},{"style":143},[808],{"type":30,"value":619},{"type":25,"tag":130,"props":810,"children":811},{"style":209},[812],{"type":30,"value":813},"0x0107",{"type":25,"tag":130,"props":815,"children":816},{"style":143},[817],{"type":30,"value":629},{"type":25,"tag":130,"props":819,"children":820},{"style":154},[821],{"type":30,"value":822},"\"pv1_voltage\"",{"type":25,"tag":130,"props":824,"children":825},{"style":143},[826],{"type":30,"value":827},",      scale: ",{"type":25,"tag":130,"props":829,"children":830},{"style":209},[831],{"type":30,"value":709},{"type":25,"tag":130,"props":833,"children":834},{"style":143},[835],{"type":30,"value":714},{"type":25,"tag":130,"props":837,"children":838},{"style":154},[839],{"type":30,"value":719},{"type":25,"tag":130,"props":841,"children":842},{"style":143},[843],{"type":30,"value":658},{"type":25,"tag":130,"props":845,"children":846},{"style":154},[847],{"type":30,"value":728},{"type":25,"tag":130,"props":849,"children":850},{"style":143},[851],{"type":30,"value":668},{"type":25,"tag":130,"props":853,"children":854},{"style":154},[855],{"type":30,"value":673},{"type":25,"tag":130,"props":857,"children":858},{"style":143},[859],{"type":30,"value":678},{"type":25,"tag":130,"props":861,"children":863},{"class":132,"line":862},15,[864,868,873,877,882,886,890,894,898,902,906,910,914],{"type":25,"tag":130,"props":865,"children":866},{"style":143},[867],{"type":30,"value":619},{"type":25,"tag":130,"props":869,"children":870},{"style":209},[871],{"type":30,"value":872},"0x0108",{"type":25,"tag":130,"props":874,"children":875},{"style":143},[876],{"type":30,"value":629},{"type":25,"tag":130,"props":878,"children":879},{"style":154},[880],{"type":30,"value":881},"\"pv1_current\"",{"type":25,"tag":130,"props":883,"children":884},{"style":143},[885],{"type":30,"value":827},{"type":25,"tag":130,"props":887,"children":888},{"style":209},[889],{"type":30,"value":709},{"type":25,"tag":130,"props":891,"children":892},{"style":143},[893],{"type":30,"value":714},{"type":25,"tag":130,"props":895,"children":896},{"style":154},[897],{"type":30,"value":779},{"type":25,"tag":130,"props":899,"children":900},{"style":143},[901],{"type":30,"value":658},{"type":25,"tag":130,"props":903,"children":904},{"style":154},[905],{"type":30,"value":788},{"type":25,"tag":130,"props":907,"children":908},{"style":143},[909],{"type":30,"value":668},{"type":25,"tag":130,"props":911,"children":912},{"style":154},[913],{"type":30,"value":673},{"type":25,"tag":130,"props":915,"children":916},{"style":143},[917],{"type":30,"value":678},{"type":25,"tag":130,"props":919,"children":921},{"class":132,"line":920},16,[922,926,931,935,940,945,949,953,958,962,967,972,976],{"type":25,"tag":130,"props":923,"children":924},{"style":143},[925],{"type":30,"value":619},{"type":25,"tag":130,"props":927,"children":928},{"style":209},[929],{"type":30,"value":930},"0x0109",{"type":25,"tag":130,"props":932,"children":933},{"style":143},[934],{"type":30,"value":629},{"type":25,"tag":130,"props":936,"children":937},{"style":154},[938],{"type":30,"value":939},"\"pv1_power\"",{"type":25,"tag":130,"props":941,"children":942},{"style":143},[943],{"type":30,"value":944},",        scale: ",{"type":25,"tag":130,"props":946,"children":947},{"style":209},[948],{"type":30,"value":318},{"type":25,"tag":130,"props":950,"children":951},{"style":143},[952],{"type":30,"value":648},{"type":25,"tag":130,"props":954,"children":955},{"style":154},[956],{"type":30,"value":957},"\"W\"",{"type":25,"tag":130,"props":959,"children":960},{"style":143},[961],{"type":30,"value":658},{"type":25,"tag":130,"props":963,"children":964},{"style":154},[965],{"type":30,"value":966},"\"power\"",{"type":25,"tag":130,"props":968,"children":969},{"style":143},[970],{"type":30,"value":971},",       stateClass: ",{"type":25,"tag":130,"props":973,"children":974},{"style":154},[975],{"type":30,"value":673},{"type":25,"tag":130,"props":977,"children":978},{"style":143},[979],{"type":30,"value":678},{"type":25,"tag":130,"props":981,"children":983},{"class":132,"line":982},17,[984],{"type":25,"tag":130,"props":985,"children":987},{"style":986},"--shiki-default:#6A737D",[988],{"type":30,"value":989},"  // ...~24 more\n",{"type":25,"tag":130,"props":991,"children":993},{"class":132,"line":992},18,[994],{"type":25,"tag":130,"props":995,"children":996},{"style":143},[997],{"type":30,"value":998},"];\n",{"type":25,"tag":26,"props":1000,"children":1001},{},[1002],{"type":30,"value":1003},"Having types here pays off immediately. In Python, a missing key in a dictionary fails at runtime. In TypeScript with strict mode, the wrong shape fails at compile time. I caught two register scaling bugs during the rewrite that would have silently produced wrong numbers in the original.",{"type":25,"tag":1005,"props":1006,"children":1008},"h3",{"id":1007},"auto-grouping-contiguous-registers",[1009],{"type":30,"value":1010},"Auto-Grouping Contiguous Registers",{"type":25,"tag":26,"props":1012,"children":1013},{},[1014],{"type":30,"value":1015},"The same contiguous grouping logic from the Python version is here, but typed:",{"type":25,"tag":121,"props":1017,"children":1019},{"code":1018,"language":14,"meta":7,"className":124,"style":7},"function groupContiguous(regs: RegisterDef[]): Array\u003C{ start: number; count: number; defs: RegisterDef[] }> {\n  const sorted = [...regs].sort((a, b) => a.address - b.address);\n  const blocks: Array\u003C{ start: number; count: number; defs: RegisterDef[] }> = [];\n  let group: RegisterDef[] = [sorted[0]];\n\n  for (let i = 1; i \u003C sorted.length; i++) {\n    if (sorted[i].address === sorted[i - 1].address + 1) {\n      group.push(sorted[i]);\n    } else {\n      blocks.push({ start: group[0].address, count: group.length, defs: group });\n      group = [sorted[i]];\n    }\n  }\n  blocks.push({ start: group[0].address, count: group.length, defs: group });\n  return blocks;\n}\n",[1020],{"type":25,"tag":69,"props":1021,"children":1022},{"__ignoreMap":7},[1023,1125,1206,1288,1332,1339,1406,1455,1473,1490,1525,1542,1550,1558,1590,1603],{"type":25,"tag":130,"props":1024,"children":1025},{"class":132,"line":133},[1026,1031,1036,1040,1045,1049,1053,1058,1062,1067,1072,1077,1081,1085,1090,1095,1099,1103,1107,1112,1116,1120],{"type":25,"tag":130,"props":1027,"children":1028},{"style":137},[1029],{"type":30,"value":1030},"function",{"type":25,"tag":130,"props":1032,"children":1033},{"style":225},[1034],{"type":30,"value":1035}," groupContiguous",{"type":25,"tag":130,"props":1037,"children":1038},{"style":143},[1039],{"type":30,"value":257},{"type":25,"tag":130,"props":1041,"children":1042},{"style":442},[1043],{"type":30,"value":1044},"regs",{"type":25,"tag":130,"props":1046,"children":1047},{"style":137},[1048],{"type":30,"value":450},{"type":25,"tag":130,"props":1050,"children":1051},{"style":225},[1052],{"type":30,"value":427},{"type":25,"tag":130,"props":1054,"children":1055},{"style":143},[1056],{"type":30,"value":1057},"[])",{"type":25,"tag":130,"props":1059,"children":1060},{"style":137},[1061],{"type":30,"value":450},{"type":25,"tag":130,"props":1063,"children":1064},{"style":225},[1065],{"type":30,"value":1066}," Array",{"type":25,"tag":130,"props":1068,"children":1069},{"style":143},[1070],{"type":30,"value":1071},"\u003C{ ",{"type":25,"tag":130,"props":1073,"children":1074},{"style":442},[1075],{"type":30,"value":1076},"start",{"type":25,"tag":130,"props":1078,"children":1079},{"style":137},[1080],{"type":30,"value":450},{"type":25,"tag":130,"props":1082,"children":1083},{"style":209},[1084],{"type":30,"value":455},{"type":25,"tag":130,"props":1086,"children":1087},{"style":143},[1088],{"type":30,"value":1089},"; ",{"type":25,"tag":130,"props":1091,"children":1092},{"style":442},[1093],{"type":30,"value":1094},"count",{"type":25,"tag":130,"props":1096,"children":1097},{"style":137},[1098],{"type":30,"value":450},{"type":25,"tag":130,"props":1100,"children":1101},{"style":209},[1102],{"type":30,"value":455},{"type":25,"tag":130,"props":1104,"children":1105},{"style":143},[1106],{"type":30,"value":1089},{"type":25,"tag":130,"props":1108,"children":1109},{"style":442},[1110],{"type":30,"value":1111},"defs",{"type":25,"tag":130,"props":1113,"children":1114},{"style":137},[1115],{"type":30,"value":450},{"type":25,"tag":130,"props":1117,"children":1118},{"style":225},[1119],{"type":30,"value":427},{"type":25,"tag":130,"props":1121,"children":1122},{"style":143},[1123],{"type":30,"value":1124},"[] }> {\n",{"type":25,"tag":130,"props":1126,"children":1127},{"class":132,"line":19},[1128,1133,1138,1142,1147,1152,1157,1162,1167,1171,1176,1181,1186,1191,1196,1201],{"type":25,"tag":130,"props":1129,"children":1130},{"style":137},[1131],{"type":30,"value":1132},"  const",{"type":25,"tag":130,"props":1134,"children":1135},{"style":209},[1136],{"type":30,"value":1137}," sorted",{"type":25,"tag":130,"props":1139,"children":1140},{"style":137},[1141],{"type":30,"value":217},{"type":25,"tag":130,"props":1143,"children":1144},{"style":143},[1145],{"type":30,"value":1146}," [",{"type":25,"tag":130,"props":1148,"children":1149},{"style":137},[1150],{"type":30,"value":1151},"...",{"type":25,"tag":130,"props":1153,"children":1154},{"style":143},[1155],{"type":30,"value":1156},"regs].",{"type":25,"tag":130,"props":1158,"children":1159},{"style":225},[1160],{"type":30,"value":1161},"sort",{"type":25,"tag":130,"props":1163,"children":1164},{"style":143},[1165],{"type":30,"value":1166},"((",{"type":25,"tag":130,"props":1168,"children":1169},{"style":442},[1170],{"type":30,"value":33},{"type":25,"tag":130,"props":1172,"children":1173},{"style":143},[1174],{"type":30,"value":1175},", ",{"type":25,"tag":130,"props":1177,"children":1178},{"style":442},[1179],{"type":30,"value":1180},"b",{"type":25,"tag":130,"props":1182,"children":1183},{"style":143},[1184],{"type":30,"value":1185},") ",{"type":25,"tag":130,"props":1187,"children":1188},{"style":137},[1189],{"type":30,"value":1190},"=>",{"type":25,"tag":130,"props":1192,"children":1193},{"style":143},[1194],{"type":30,"value":1195}," a.address ",{"type":25,"tag":130,"props":1197,"children":1198},{"style":137},[1199],{"type":30,"value":1200},"-",{"type":25,"tag":130,"props":1202,"children":1203},{"style":143},[1204],{"type":30,"value":1205}," b.address);\n",{"type":25,"tag":130,"props":1207,"children":1208},{"class":132,"line":190},[1209,1213,1218,1222,1226,1230,1234,1238,1242,1246,1250,1254,1258,1262,1266,1270,1274,1279,1283],{"type":25,"tag":130,"props":1210,"children":1211},{"style":137},[1212],{"type":30,"value":1132},{"type":25,"tag":130,"props":1214,"children":1215},{"style":209},[1216],{"type":30,"value":1217}," blocks",{"type":25,"tag":130,"props":1219,"children":1220},{"style":137},[1221],{"type":30,"value":450},{"type":25,"tag":130,"props":1223,"children":1224},{"style":225},[1225],{"type":30,"value":1066},{"type":25,"tag":130,"props":1227,"children":1228},{"style":143},[1229],{"type":30,"value":1071},{"type":25,"tag":130,"props":1231,"children":1232},{"style":442},[1233],{"type":30,"value":1076},{"type":25,"tag":130,"props":1235,"children":1236},{"style":137},[1237],{"type":30,"value":450},{"type":25,"tag":130,"props":1239,"children":1240},{"style":209},[1241],{"type":30,"value":455},{"type":25,"tag":130,"props":1243,"children":1244},{"style":143},[1245],{"type":30,"value":1089},{"type":25,"tag":130,"props":1247,"children":1248},{"style":442},[1249],{"type":30,"value":1094},{"type":25,"tag":130,"props":1251,"children":1252},{"style":137},[1253],{"type":30,"value":450},{"type":25,"tag":130,"props":1255,"children":1256},{"style":209},[1257],{"type":30,"value":455},{"type":25,"tag":130,"props":1259,"children":1260},{"style":143},[1261],{"type":30,"value":1089},{"type":25,"tag":130,"props":1263,"children":1264},{"style":442},[1265],{"type":30,"value":1111},{"type":25,"tag":130,"props":1267,"children":1268},{"style":137},[1269],{"type":30,"value":450},{"type":25,"tag":130,"props":1271,"children":1272},{"style":225},[1273],{"type":30,"value":427},{"type":25,"tag":130,"props":1275,"children":1276},{"style":143},[1277],{"type":30,"value":1278},"[] }> ",{"type":25,"tag":130,"props":1280,"children":1281},{"style":137},[1282],{"type":30,"value":606},{"type":25,"tag":130,"props":1284,"children":1285},{"style":143},[1286],{"type":30,"value":1287}," [];\n",{"type":25,"tag":130,"props":1289,"children":1290},{"class":132,"line":200},[1291,1296,1301,1305,1309,1313,1317,1322,1327],{"type":25,"tag":130,"props":1292,"children":1293},{"style":137},[1294],{"type":30,"value":1295},"  let",{"type":25,"tag":130,"props":1297,"children":1298},{"style":143},[1299],{"type":30,"value":1300}," group",{"type":25,"tag":130,"props":1302,"children":1303},{"style":137},[1304],{"type":30,"value":450},{"type":25,"tag":130,"props":1306,"children":1307},{"style":225},[1308],{"type":30,"value":427},{"type":25,"tag":130,"props":1310,"children":1311},{"style":143},[1312],{"type":30,"value":601},{"type":25,"tag":130,"props":1314,"children":1315},{"style":137},[1316],{"type":30,"value":606},{"type":25,"tag":130,"props":1318,"children":1319},{"style":143},[1320],{"type":30,"value":1321}," [sorted[",{"type":25,"tag":130,"props":1323,"children":1324},{"style":209},[1325],{"type":30,"value":1326},"0",{"type":25,"tag":130,"props":1328,"children":1329},{"style":143},[1330],{"type":30,"value":1331},"]];\n",{"type":25,"tag":130,"props":1333,"children":1334},{"class":132,"line":236},[1335],{"type":25,"tag":130,"props":1336,"children":1337},{"emptyLinePlaceholder":194},[1338],{"type":30,"value":197},{"type":25,"tag":130,"props":1340,"children":1341},{"class":132,"line":270},[1342,1347,1352,1357,1362,1366,1371,1376,1381,1386,1391,1396,1401],{"type":25,"tag":130,"props":1343,"children":1344},{"style":137},[1345],{"type":30,"value":1346},"  for",{"type":25,"tag":130,"props":1348,"children":1349},{"style":143},[1350],{"type":30,"value":1351}," (",{"type":25,"tag":130,"props":1353,"children":1354},{"style":137},[1355],{"type":30,"value":1356},"let",{"type":25,"tag":130,"props":1358,"children":1359},{"style":143},[1360],{"type":30,"value":1361}," i ",{"type":25,"tag":130,"props":1363,"children":1364},{"style":137},[1365],{"type":30,"value":606},{"type":25,"tag":130,"props":1367,"children":1368},{"style":209},[1369],{"type":30,"value":1370}," 1",{"type":25,"tag":130,"props":1372,"children":1373},{"style":143},[1374],{"type":30,"value":1375},"; i ",{"type":25,"tag":130,"props":1377,"children":1378},{"style":137},[1379],{"type":30,"value":1380},"\u003C",{"type":25,"tag":130,"props":1382,"children":1383},{"style":143},[1384],{"type":30,"value":1385}," sorted.",{"type":25,"tag":130,"props":1387,"children":1388},{"style":209},[1389],{"type":30,"value":1390},"length",{"type":25,"tag":130,"props":1392,"children":1393},{"style":143},[1394],{"type":30,"value":1395},"; i",{"type":25,"tag":130,"props":1397,"children":1398},{"style":137},[1399],{"type":30,"value":1400},"++",{"type":25,"tag":130,"props":1402,"children":1403},{"style":143},[1404],{"type":30,"value":1405},") {\n",{"type":25,"tag":130,"props":1407,"children":1408},{"class":132,"line":289},[1409,1414,1419,1424,1429,1433,1437,1442,1447,1451],{"type":25,"tag":130,"props":1410,"children":1411},{"style":137},[1412],{"type":30,"value":1413},"    if",{"type":25,"tag":130,"props":1415,"children":1416},{"style":143},[1417],{"type":30,"value":1418}," (sorted[i].address ",{"type":25,"tag":130,"props":1420,"children":1421},{"style":137},[1422],{"type":30,"value":1423},"===",{"type":25,"tag":130,"props":1425,"children":1426},{"style":143},[1427],{"type":30,"value":1428}," sorted[i ",{"type":25,"tag":130,"props":1430,"children":1431},{"style":137},[1432],{"type":30,"value":1200},{"type":25,"tag":130,"props":1434,"children":1435},{"style":209},[1436],{"type":30,"value":1370},{"type":25,"tag":130,"props":1438,"children":1439},{"style":143},[1440],{"type":30,"value":1441},"].address ",{"type":25,"tag":130,"props":1443,"children":1444},{"style":137},[1445],{"type":30,"value":1446},"+",{"type":25,"tag":130,"props":1448,"children":1449},{"style":209},[1450],{"type":30,"value":1370},{"type":25,"tag":130,"props":1452,"children":1453},{"style":143},[1454],{"type":30,"value":1405},{"type":25,"tag":130,"props":1456,"children":1457},{"class":132,"line":307},[1458,1463,1468],{"type":25,"tag":130,"props":1459,"children":1460},{"style":143},[1461],{"type":30,"value":1462},"      group.",{"type":25,"tag":130,"props":1464,"children":1465},{"style":225},[1466],{"type":30,"value":1467},"push",{"type":25,"tag":130,"props":1469,"children":1470},{"style":143},[1471],{"type":30,"value":1472},"(sorted[i]);\n",{"type":25,"tag":130,"props":1474,"children":1475},{"class":132,"line":325},[1476,1481,1486],{"type":25,"tag":130,"props":1477,"children":1478},{"style":143},[1479],{"type":30,"value":1480},"    } ",{"type":25,"tag":130,"props":1482,"children":1483},{"style":137},[1484],{"type":30,"value":1485},"else",{"type":25,"tag":130,"props":1487,"children":1488},{"style":143},[1489],{"type":30,"value":436},{"type":25,"tag":130,"props":1491,"children":1492},{"class":132,"line":343},[1493,1498,1502,1507,1511,1516,1520],{"type":25,"tag":130,"props":1494,"children":1495},{"style":143},[1496],{"type":30,"value":1497},"      blocks.",{"type":25,"tag":130,"props":1499,"children":1500},{"style":225},[1501],{"type":30,"value":1467},{"type":25,"tag":130,"props":1503,"children":1504},{"style":143},[1505],{"type":30,"value":1506},"({ start: group[",{"type":25,"tag":130,"props":1508,"children":1509},{"style":209},[1510],{"type":30,"value":1326},{"type":25,"tag":130,"props":1512,"children":1513},{"style":143},[1514],{"type":30,"value":1515},"].address, count: group.",{"type":25,"tag":130,"props":1517,"children":1518},{"style":209},[1519],{"type":30,"value":1390},{"type":25,"tag":130,"props":1521,"children":1522},{"style":143},[1523],{"type":30,"value":1524},", defs: group });\n",{"type":25,"tag":130,"props":1526,"children":1527},{"class":132,"line":352},[1528,1533,1537],{"type":25,"tag":130,"props":1529,"children":1530},{"style":143},[1531],{"type":30,"value":1532},"      group ",{"type":25,"tag":130,"props":1534,"children":1535},{"style":137},[1536],{"type":30,"value":606},{"type":25,"tag":130,"props":1538,"children":1539},{"style":143},[1540],{"type":30,"value":1541}," [sorted[i]];\n",{"type":25,"tag":130,"props":1543,"children":1544},{"class":132,"line":379},[1545],{"type":25,"tag":130,"props":1546,"children":1547},{"style":143},[1548],{"type":30,"value":1549},"    }\n",{"type":25,"tag":130,"props":1551,"children":1552},{"class":132,"line":743},[1553],{"type":25,"tag":130,"props":1554,"children":1555},{"style":143},[1556],{"type":30,"value":1557},"  }\n",{"type":25,"tag":130,"props":1559,"children":1560},{"class":132,"line":803},[1561,1566,1570,1574,1578,1582,1586],{"type":25,"tag":130,"props":1562,"children":1563},{"style":143},[1564],{"type":30,"value":1565},"  blocks.",{"type":25,"tag":130,"props":1567,"children":1568},{"style":225},[1569],{"type":30,"value":1467},{"type":25,"tag":130,"props":1571,"children":1572},{"style":143},[1573],{"type":30,"value":1506},{"type":25,"tag":130,"props":1575,"children":1576},{"style":209},[1577],{"type":30,"value":1326},{"type":25,"tag":130,"props":1579,"children":1580},{"style":143},[1581],{"type":30,"value":1515},{"type":25,"tag":130,"props":1583,"children":1584},{"style":209},[1585],{"type":30,"value":1390},{"type":25,"tag":130,"props":1587,"children":1588},{"style":143},[1589],{"type":30,"value":1524},{"type":25,"tag":130,"props":1591,"children":1592},{"class":132,"line":862},[1593,1598],{"type":25,"tag":130,"props":1594,"children":1595},{"style":137},[1596],{"type":30,"value":1597},"  return",{"type":25,"tag":130,"props":1599,"children":1600},{"style":143},[1601],{"type":30,"value":1602}," blocks;\n",{"type":25,"tag":130,"props":1604,"children":1605},{"class":132,"line":920},[1606],{"type":25,"tag":130,"props":1607,"children":1608},{"style":143},[1609],{"type":30,"value":1610},"}\n",{"type":25,"tag":26,"props":1612,"children":1613},{},[1614],{"type":30,"value":1615},"This runs once at startup, so there's no overhead per poll cycle.",{"type":25,"tag":78,"props":1617,"children":1619},{"id":1618},"mqtt-replaces-the-custom-api",[1620],{"type":30,"value":1621},"MQTT Replaces the Custom API",{"type":25,"tag":26,"props":1623,"children":1624},{},[1625],{"type":30,"value":1626},"In the original stack, the TypeScript API was the integration layer. Anything that wanted solar data had to know the API's endpoints and speak HTTP.",{"type":25,"tag":26,"props":1628,"children":1629},{},[1630],{"type":30,"value":1631},"The new integration layer is MQTT, specifically Eclipse Mosquitto running in the same Docker Compose stack. The poller publishes each metric as a retained message on a per-sensor topic:",{"type":25,"tag":121,"props":1633,"children":1635},{"code":1634},"solar/battery_soc        → \"87\"\nsolar/battery_voltage    → \"52.3\"\nsolar/pv1_power          → \"1840\"\nsolar/inverter_state     → \"charging\"\n",[1636],{"type":25,"tag":69,"props":1637,"children":1638},{"__ignoreMap":7},[1639],{"type":30,"value":1634},{"type":25,"tag":26,"props":1641,"children":1642},{},[1643],{"type":30,"value":1644},"Retained messages mean any new subscriber immediately gets the last known value without waiting for the next poll cycle. Everything that wants solar data subscribes to the relevant topics. Nothing needs to know about the poller, TimescaleDB, or Modbus. MQTT is the contract.",{"type":25,"tag":78,"props":1646,"children":1648},{"id":1647},"home-assistant-auto-discovery",[1649],{"type":30,"value":1650},"Home Assistant Auto-Discovery",{"type":25,"tag":26,"props":1652,"children":1653},{},[1654],{"type":30,"value":1655},"This is the part that changed the maintenance picture most dramatically.",{"type":25,"tag":26,"props":1657,"children":1658},{},[1659],{"type":30,"value":1660},"The poller publishes Home Assistant MQTT discovery configs at startup. Each sensor definition maps directly to a discovery message:",{"type":25,"tag":121,"props":1662,"children":1664},{"code":1663,"language":14,"meta":7,"className":124,"style":7},"function publishDiscovery(mqttClient: mqtt.MqttClient, reg: RegisterDef): void {\n  const config = {\n    name: reg.name.replace(/_/g, \" \"),\n    state_topic: `solar/${reg.name}`,\n    unit_of_measurement: reg.unit,\n    device_class: reg.deviceClass,\n    state_class: reg.stateClass,\n    unique_id: `sph5048_${reg.name}`,\n    device: {\n      identifiers: [\"sph5048\"],\n      name: \"Solar Inverter\",\n      model: \"SPH5048\",\n      manufacturer: \"Sungoldpower\",\n    },\n  };\n\n  mqttClient.publish(\n    `homeassistant/sensor/sph5048/${reg.name}/config`,\n    JSON.stringify(config),\n    { retain: true }\n  );\n}\n",[1665],{"type":25,"tag":69,"props":1666,"children":1667},{"__ignoreMap":7},[1668,1743,1763,1814,1849,1857,1865,1873,1906,1914,1932,1949,1966,1983,1991,1999,2006,2024,2053,2076,2095,2104],{"type":25,"tag":130,"props":1669,"children":1670},{"class":132,"line":133},[1671,1675,1680,1684,1689,1693,1698,1703,1708,1712,1717,1721,1725,1730,1734,1739],{"type":25,"tag":130,"props":1672,"children":1673},{"style":137},[1674],{"type":30,"value":1030},{"type":25,"tag":130,"props":1676,"children":1677},{"style":225},[1678],{"type":30,"value":1679}," publishDiscovery",{"type":25,"tag":130,"props":1681,"children":1682},{"style":143},[1683],{"type":30,"value":257},{"type":25,"tag":130,"props":1685,"children":1686},{"style":442},[1687],{"type":30,"value":1688},"mqttClient",{"type":25,"tag":130,"props":1690,"children":1691},{"style":137},[1692],{"type":30,"value":450},{"type":25,"tag":130,"props":1694,"children":1695},{"style":225},[1696],{"type":30,"value":1697}," mqtt",{"type":25,"tag":130,"props":1699,"children":1700},{"style":143},[1701],{"type":30,"value":1702},".",{"type":25,"tag":130,"props":1704,"children":1705},{"style":225},[1706],{"type":30,"value":1707},"MqttClient",{"type":25,"tag":130,"props":1709,"children":1710},{"style":143},[1711],{"type":30,"value":1175},{"type":25,"tag":130,"props":1713,"children":1714},{"style":442},[1715],{"type":30,"value":1716},"reg",{"type":25,"tag":130,"props":1718,"children":1719},{"style":137},[1720],{"type":30,"value":450},{"type":25,"tag":130,"props":1722,"children":1723},{"style":225},[1724],{"type":30,"value":427},{"type":25,"tag":130,"props":1726,"children":1727},{"style":143},[1728],{"type":30,"value":1729},")",{"type":25,"tag":130,"props":1731,"children":1732},{"style":137},[1733],{"type":30,"value":450},{"type":25,"tag":130,"props":1735,"children":1736},{"style":209},[1737],{"type":30,"value":1738}," void",{"type":25,"tag":130,"props":1740,"children":1741},{"style":143},[1742],{"type":30,"value":436},{"type":25,"tag":130,"props":1744,"children":1745},{"class":132,"line":19},[1746,1750,1755,1759],{"type":25,"tag":130,"props":1747,"children":1748},{"style":137},[1749],{"type":30,"value":1132},{"type":25,"tag":130,"props":1751,"children":1752},{"style":209},[1753],{"type":30,"value":1754}," config",{"type":25,"tag":130,"props":1756,"children":1757},{"style":137},[1758],{"type":30,"value":217},{"type":25,"tag":130,"props":1760,"children":1761},{"style":143},[1762],{"type":30,"value":436},{"type":25,"tag":130,"props":1764,"children":1765},{"class":132,"line":190},[1766,1771,1776,1780,1785,1791,1795,1800,1804,1809],{"type":25,"tag":130,"props":1767,"children":1768},{"style":143},[1769],{"type":30,"value":1770},"    name: reg.name.",{"type":25,"tag":130,"props":1772,"children":1773},{"style":225},[1774],{"type":30,"value":1775},"replace",{"type":25,"tag":130,"props":1777,"children":1778},{"style":143},[1779],{"type":30,"value":257},{"type":25,"tag":130,"props":1781,"children":1782},{"style":154},[1783],{"type":30,"value":1784},"/",{"type":25,"tag":130,"props":1786,"children":1788},{"style":1787},"--shiki-default:#DBEDFF",[1789],{"type":30,"value":1790},"_",{"type":25,"tag":130,"props":1792,"children":1793},{"style":154},[1794],{"type":30,"value":1784},{"type":25,"tag":130,"props":1796,"children":1797},{"style":137},[1798],{"type":30,"value":1799},"g",{"type":25,"tag":130,"props":1801,"children":1802},{"style":143},[1803],{"type":30,"value":1175},{"type":25,"tag":130,"props":1805,"children":1806},{"style":154},[1807],{"type":30,"value":1808},"\" \"",{"type":25,"tag":130,"props":1810,"children":1811},{"style":143},[1812],{"type":30,"value":1813},"),\n",{"type":25,"tag":130,"props":1815,"children":1816},{"class":132,"line":200},[1817,1822,1827,1831,1835,1840,1845],{"type":25,"tag":130,"props":1818,"children":1819},{"style":143},[1820],{"type":30,"value":1821},"    state_topic: ",{"type":25,"tag":130,"props":1823,"children":1824},{"style":154},[1825],{"type":30,"value":1826},"`solar/${",{"type":25,"tag":130,"props":1828,"children":1829},{"style":143},[1830],{"type":30,"value":1716},{"type":25,"tag":130,"props":1832,"children":1833},{"style":154},[1834],{"type":30,"value":1702},{"type":25,"tag":130,"props":1836,"children":1837},{"style":143},[1838],{"type":30,"value":1839},"name",{"type":25,"tag":130,"props":1841,"children":1842},{"style":154},[1843],{"type":30,"value":1844},"}`",{"type":25,"tag":130,"props":1846,"children":1847},{"style":143},[1848],{"type":30,"value":286},{"type":25,"tag":130,"props":1850,"children":1851},{"class":132,"line":236},[1852],{"type":25,"tag":130,"props":1853,"children":1854},{"style":143},[1855],{"type":30,"value":1856},"    unit_of_measurement: reg.unit,\n",{"type":25,"tag":130,"props":1858,"children":1859},{"class":132,"line":270},[1860],{"type":25,"tag":130,"props":1861,"children":1862},{"style":143},[1863],{"type":30,"value":1864},"    device_class: reg.deviceClass,\n",{"type":25,"tag":130,"props":1866,"children":1867},{"class":132,"line":289},[1868],{"type":25,"tag":130,"props":1869,"children":1870},{"style":143},[1871],{"type":30,"value":1872},"    state_class: reg.stateClass,\n",{"type":25,"tag":130,"props":1874,"children":1875},{"class":132,"line":307},[1876,1881,1886,1890,1894,1898,1902],{"type":25,"tag":130,"props":1877,"children":1878},{"style":143},[1879],{"type":30,"value":1880},"    unique_id: ",{"type":25,"tag":130,"props":1882,"children":1883},{"style":154},[1884],{"type":30,"value":1885},"`sph5048_${",{"type":25,"tag":130,"props":1887,"children":1888},{"style":143},[1889],{"type":30,"value":1716},{"type":25,"tag":130,"props":1891,"children":1892},{"style":154},[1893],{"type":30,"value":1702},{"type":25,"tag":130,"props":1895,"children":1896},{"style":143},[1897],{"type":30,"value":1839},{"type":25,"tag":130,"props":1899,"children":1900},{"style":154},[1901],{"type":30,"value":1844},{"type":25,"tag":130,"props":1903,"children":1904},{"style":143},[1905],{"type":30,"value":286},{"type":25,"tag":130,"props":1907,"children":1908},{"class":132,"line":325},[1909],{"type":25,"tag":130,"props":1910,"children":1911},{"style":143},[1912],{"type":30,"value":1913},"    device: {\n",{"type":25,"tag":130,"props":1915,"children":1916},{"class":132,"line":343},[1917,1922,1927],{"type":25,"tag":130,"props":1918,"children":1919},{"style":143},[1920],{"type":30,"value":1921},"      identifiers: [",{"type":25,"tag":130,"props":1923,"children":1924},{"style":154},[1925],{"type":30,"value":1926},"\"sph5048\"",{"type":25,"tag":130,"props":1928,"children":1929},{"style":143},[1930],{"type":30,"value":1931},"],\n",{"type":25,"tag":130,"props":1933,"children":1934},{"class":132,"line":352},[1935,1940,1945],{"type":25,"tag":130,"props":1936,"children":1937},{"style":143},[1938],{"type":30,"value":1939},"      name: ",{"type":25,"tag":130,"props":1941,"children":1942},{"style":154},[1943],{"type":30,"value":1944},"\"Solar Inverter\"",{"type":25,"tag":130,"props":1946,"children":1947},{"style":143},[1948],{"type":30,"value":286},{"type":25,"tag":130,"props":1950,"children":1951},{"class":132,"line":379},[1952,1957,1962],{"type":25,"tag":130,"props":1953,"children":1954},{"style":143},[1955],{"type":30,"value":1956},"      model: ",{"type":25,"tag":130,"props":1958,"children":1959},{"style":154},[1960],{"type":30,"value":1961},"\"SPH5048\"",{"type":25,"tag":130,"props":1963,"children":1964},{"style":143},[1965],{"type":30,"value":286},{"type":25,"tag":130,"props":1967,"children":1968},{"class":132,"line":743},[1969,1974,1979],{"type":25,"tag":130,"props":1970,"children":1971},{"style":143},[1972],{"type":30,"value":1973},"      manufacturer: ",{"type":25,"tag":130,"props":1975,"children":1976},{"style":154},[1977],{"type":30,"value":1978},"\"Sungoldpower\"",{"type":25,"tag":130,"props":1980,"children":1981},{"style":143},[1982],{"type":30,"value":286},{"type":25,"tag":130,"props":1984,"children":1985},{"class":132,"line":803},[1986],{"type":25,"tag":130,"props":1987,"children":1988},{"style":143},[1989],{"type":30,"value":1990},"    },\n",{"type":25,"tag":130,"props":1992,"children":1993},{"class":132,"line":862},[1994],{"type":25,"tag":130,"props":1995,"children":1996},{"style":143},[1997],{"type":30,"value":1998},"  };\n",{"type":25,"tag":130,"props":2000,"children":2001},{"class":132,"line":920},[2002],{"type":25,"tag":130,"props":2003,"children":2004},{"emptyLinePlaceholder":194},[2005],{"type":30,"value":197},{"type":25,"tag":130,"props":2007,"children":2008},{"class":132,"line":982},[2009,2014,2019],{"type":25,"tag":130,"props":2010,"children":2011},{"style":143},[2012],{"type":30,"value":2013},"  mqttClient.",{"type":25,"tag":130,"props":2015,"children":2016},{"style":225},[2017],{"type":30,"value":2018},"publish",{"type":25,"tag":130,"props":2020,"children":2021},{"style":143},[2022],{"type":30,"value":2023},"(\n",{"type":25,"tag":130,"props":2025,"children":2026},{"class":132,"line":992},[2027,2032,2036,2040,2044,2049],{"type":25,"tag":130,"props":2028,"children":2029},{"style":154},[2030],{"type":30,"value":2031},"    `homeassistant/sensor/sph5048/${",{"type":25,"tag":130,"props":2033,"children":2034},{"style":143},[2035],{"type":30,"value":1716},{"type":25,"tag":130,"props":2037,"children":2038},{"style":154},[2039],{"type":30,"value":1702},{"type":25,"tag":130,"props":2041,"children":2042},{"style":143},[2043],{"type":30,"value":1839},{"type":25,"tag":130,"props":2045,"children":2046},{"style":154},[2047],{"type":30,"value":2048},"}/config`",{"type":25,"tag":130,"props":2050,"children":2051},{"style":143},[2052],{"type":30,"value":286},{"type":25,"tag":130,"props":2054,"children":2056},{"class":132,"line":2055},19,[2057,2062,2066,2071],{"type":25,"tag":130,"props":2058,"children":2059},{"style":209},[2060],{"type":30,"value":2061},"    JSON",{"type":25,"tag":130,"props":2063,"children":2064},{"style":143},[2065],{"type":30,"value":1702},{"type":25,"tag":130,"props":2067,"children":2068},{"style":225},[2069],{"type":30,"value":2070},"stringify",{"type":25,"tag":130,"props":2072,"children":2073},{"style":143},[2074],{"type":30,"value":2075},"(config),\n",{"type":25,"tag":130,"props":2077,"children":2079},{"class":132,"line":2078},20,[2080,2085,2090],{"type":25,"tag":130,"props":2081,"children":2082},{"style":143},[2083],{"type":30,"value":2084},"    { retain: ",{"type":25,"tag":130,"props":2086,"children":2087},{"style":209},[2088],{"type":30,"value":2089},"true",{"type":25,"tag":130,"props":2091,"children":2092},{"style":143},[2093],{"type":30,"value":2094}," }\n",{"type":25,"tag":130,"props":2096,"children":2098},{"class":132,"line":2097},21,[2099],{"type":25,"tag":130,"props":2100,"children":2101},{"style":143},[2102],{"type":30,"value":2103},"  );\n",{"type":25,"tag":130,"props":2105,"children":2107},{"class":132,"line":2106},22,[2108],{"type":25,"tag":130,"props":2109,"children":2110},{"style":143},[2111],{"type":30,"value":1610},{"type":25,"tag":26,"props":2113,"children":2114},{},[2115],{"type":30,"value":2116},"When Home Assistant sees these discovery messages, it creates all 30+ sensors automatically under a \"Solar Inverter\" device. No YAML config files. No manual entity setup. They just appear.",{"type":25,"tag":26,"props":2118,"children":2119},{},[2120,2122,2128],{"type":30,"value":2121},"Adding a new register to the ",{"type":25,"tag":69,"props":2123,"children":2125},{"className":2124},[],[2126],{"type":30,"value":2127},"REGISTERS",{"type":30,"value":2129}," array automatically adds a new sensor in Home Assistant on the next container restart. Removing a register removes it. The definition is the source of truth.",{"type":25,"tag":78,"props":2131,"children":2133},{"id":2132},"energy-tracking-with-trapezoidal-integration",[2134],{"type":30,"value":2135},"Energy Tracking With Trapezoidal Integration",{"type":25,"tag":26,"props":2137,"children":2138},{},[2139,2141,2147],{"type":30,"value":2140},"The original stack had no energy tracking. It stored power values (watts) but never accumulated them into energy values (kWh). That meant I couldn't use Home Assistant's Energy Dashboard, which needs ",{"type":25,"tag":69,"props":2142,"children":2144},{"className":2143},[],[2145],{"type":30,"value":2146},"total_increasing",{"type":30,"value":2148}," sensors with kWh units.",{"type":25,"tag":26,"props":2150,"children":2151},{},[2152],{"type":30,"value":2153},"The rebuild adds software energy accumulators. Every poll cycle, the poller calculates the energy added since the last poll using the trapezoidal rule: average the current and previous power readings, multiply by the elapsed time interval.",{"type":25,"tag":121,"props":2155,"children":2157},{"code":2156,"language":14,"meta":7,"className":124,"style":7},"class EnergyAccumulator {\n  private totalKwh: number;\n  private lastPowerW: number | null;\n  private lastTs: number | null;\n\n  constructor(initialKwh = 0) {\n    this.totalKwh = initialKwh;\n    this.lastPowerW = null;\n    this.lastTs = null;\n  }\n\n  update(powerW: number, nowMs: number): number {\n    if (this.lastPowerW !== null && this.lastTs !== null) {\n      const dtHours = (nowMs - this.lastTs) / 3_600_000;\n      const avgW = (powerW + this.lastPowerW) / 2;\n      this.totalKwh += (avgW * dtHours) / 1000;\n    }\n    this.lastPowerW = powerW;\n    this.lastTs = nowMs;\n    return this.totalKwh;\n  }\n\n  get value(): number {\n    return this.totalKwh;\n  }\n}\n",[2158],{"type":25,"tag":69,"props":2159,"children":2160},{"__ignoreMap":7},[2161,2178,2203,2237,2269,2276,2306,2328,2352,2376,2383,2390,2448,2503,2551,2598,2643,2650,2670,2690,2707,2714,2721,2752,2768,2776],{"type":25,"tag":130,"props":2162,"children":2163},{"class":132,"line":133},[2164,2169,2174],{"type":25,"tag":130,"props":2165,"children":2166},{"style":137},[2167],{"type":30,"value":2168},"class",{"type":25,"tag":130,"props":2170,"children":2171},{"style":225},[2172],{"type":30,"value":2173}," EnergyAccumulator",{"type":25,"tag":130,"props":2175,"children":2176},{"style":143},[2177],{"type":30,"value":436},{"type":25,"tag":130,"props":2179,"children":2180},{"class":132,"line":19},[2181,2186,2191,2195,2199],{"type":25,"tag":130,"props":2182,"children":2183},{"style":137},[2184],{"type":30,"value":2185},"  private",{"type":25,"tag":130,"props":2187,"children":2188},{"style":442},[2189],{"type":30,"value":2190}," totalKwh",{"type":25,"tag":130,"props":2192,"children":2193},{"style":137},[2194],{"type":30,"value":450},{"type":25,"tag":130,"props":2196,"children":2197},{"style":209},[2198],{"type":30,"value":455},{"type":25,"tag":130,"props":2200,"children":2201},{"style":143},[2202],{"type":30,"value":162},{"type":25,"tag":130,"props":2204,"children":2205},{"class":132,"line":190},[2206,2210,2215,2219,2223,2228,2233],{"type":25,"tag":130,"props":2207,"children":2208},{"style":137},[2209],{"type":30,"value":2185},{"type":25,"tag":130,"props":2211,"children":2212},{"style":442},[2213],{"type":30,"value":2214}," lastPowerW",{"type":25,"tag":130,"props":2216,"children":2217},{"style":137},[2218],{"type":30,"value":450},{"type":25,"tag":130,"props":2220,"children":2221},{"style":209},[2222],{"type":30,"value":455},{"type":25,"tag":130,"props":2224,"children":2225},{"style":137},[2226],{"type":30,"value":2227}," |",{"type":25,"tag":130,"props":2229,"children":2230},{"style":209},[2231],{"type":30,"value":2232}," null",{"type":25,"tag":130,"props":2234,"children":2235},{"style":143},[2236],{"type":30,"value":162},{"type":25,"tag":130,"props":2238,"children":2239},{"class":132,"line":200},[2240,2244,2249,2253,2257,2261,2265],{"type":25,"tag":130,"props":2241,"children":2242},{"style":137},[2243],{"type":30,"value":2185},{"type":25,"tag":130,"props":2245,"children":2246},{"style":442},[2247],{"type":30,"value":2248}," lastTs",{"type":25,"tag":130,"props":2250,"children":2251},{"style":137},[2252],{"type":30,"value":450},{"type":25,"tag":130,"props":2254,"children":2255},{"style":209},[2256],{"type":30,"value":455},{"type":25,"tag":130,"props":2258,"children":2259},{"style":137},[2260],{"type":30,"value":2227},{"type":25,"tag":130,"props":2262,"children":2263},{"style":209},[2264],{"type":30,"value":2232},{"type":25,"tag":130,"props":2266,"children":2267},{"style":143},[2268],{"type":30,"value":162},{"type":25,"tag":130,"props":2270,"children":2271},{"class":132,"line":236},[2272],{"type":25,"tag":130,"props":2273,"children":2274},{"emptyLinePlaceholder":194},[2275],{"type":30,"value":197},{"type":25,"tag":130,"props":2277,"children":2278},{"class":132,"line":270},[2279,2284,2288,2293,2297,2302],{"type":25,"tag":130,"props":2280,"children":2281},{"style":137},[2282],{"type":30,"value":2283},"  constructor",{"type":25,"tag":130,"props":2285,"children":2286},{"style":143},[2287],{"type":30,"value":257},{"type":25,"tag":130,"props":2289,"children":2290},{"style":442},[2291],{"type":30,"value":2292},"initialKwh",{"type":25,"tag":130,"props":2294,"children":2295},{"style":137},[2296],{"type":30,"value":217},{"type":25,"tag":130,"props":2298,"children":2299},{"style":209},[2300],{"type":30,"value":2301}," 0",{"type":25,"tag":130,"props":2303,"children":2304},{"style":143},[2305],{"type":30,"value":1405},{"type":25,"tag":130,"props":2307,"children":2308},{"class":132,"line":289},[2309,2314,2319,2323],{"type":25,"tag":130,"props":2310,"children":2311},{"style":209},[2312],{"type":30,"value":2313},"    this",{"type":25,"tag":130,"props":2315,"children":2316},{"style":143},[2317],{"type":30,"value":2318},".totalKwh ",{"type":25,"tag":130,"props":2320,"children":2321},{"style":137},[2322],{"type":30,"value":606},{"type":25,"tag":130,"props":2324,"children":2325},{"style":143},[2326],{"type":30,"value":2327}," initialKwh;\n",{"type":25,"tag":130,"props":2329,"children":2330},{"class":132,"line":307},[2331,2335,2340,2344,2348],{"type":25,"tag":130,"props":2332,"children":2333},{"style":209},[2334],{"type":30,"value":2313},{"type":25,"tag":130,"props":2336,"children":2337},{"style":143},[2338],{"type":30,"value":2339},".lastPowerW ",{"type":25,"tag":130,"props":2341,"children":2342},{"style":137},[2343],{"type":30,"value":606},{"type":25,"tag":130,"props":2345,"children":2346},{"style":209},[2347],{"type":30,"value":2232},{"type":25,"tag":130,"props":2349,"children":2350},{"style":143},[2351],{"type":30,"value":162},{"type":25,"tag":130,"props":2353,"children":2354},{"class":132,"line":325},[2355,2359,2364,2368,2372],{"type":25,"tag":130,"props":2356,"children":2357},{"style":209},[2358],{"type":30,"value":2313},{"type":25,"tag":130,"props":2360,"children":2361},{"style":143},[2362],{"type":30,"value":2363},".lastTs ",{"type":25,"tag":130,"props":2365,"children":2366},{"style":137},[2367],{"type":30,"value":606},{"type":25,"tag":130,"props":2369,"children":2370},{"style":209},[2371],{"type":30,"value":2232},{"type":25,"tag":130,"props":2373,"children":2374},{"style":143},[2375],{"type":30,"value":162},{"type":25,"tag":130,"props":2377,"children":2378},{"class":132,"line":343},[2379],{"type":25,"tag":130,"props":2380,"children":2381},{"style":143},[2382],{"type":30,"value":1557},{"type":25,"tag":130,"props":2384,"children":2385},{"class":132,"line":352},[2386],{"type":25,"tag":130,"props":2387,"children":2388},{"emptyLinePlaceholder":194},[2389],{"type":30,"value":197},{"type":25,"tag":130,"props":2391,"children":2392},{"class":132,"line":379},[2393,2398,2402,2407,2411,2415,2419,2424,2428,2432,2436,2440,2444],{"type":25,"tag":130,"props":2394,"children":2395},{"style":225},[2396],{"type":30,"value":2397},"  update",{"type":25,"tag":130,"props":2399,"children":2400},{"style":143},[2401],{"type":30,"value":257},{"type":25,"tag":130,"props":2403,"children":2404},{"style":442},[2405],{"type":30,"value":2406},"powerW",{"type":25,"tag":130,"props":2408,"children":2409},{"style":137},[2410],{"type":30,"value":450},{"type":25,"tag":130,"props":2412,"children":2413},{"style":209},[2414],{"type":30,"value":455},{"type":25,"tag":130,"props":2416,"children":2417},{"style":143},[2418],{"type":30,"value":1175},{"type":25,"tag":130,"props":2420,"children":2421},{"style":442},[2422],{"type":30,"value":2423},"nowMs",{"type":25,"tag":130,"props":2425,"children":2426},{"style":137},[2427],{"type":30,"value":450},{"type":25,"tag":130,"props":2429,"children":2430},{"style":209},[2431],{"type":30,"value":455},{"type":25,"tag":130,"props":2433,"children":2434},{"style":143},[2435],{"type":30,"value":1729},{"type":25,"tag":130,"props":2437,"children":2438},{"style":137},[2439],{"type":30,"value":450},{"type":25,"tag":130,"props":2441,"children":2442},{"style":209},[2443],{"type":30,"value":455},{"type":25,"tag":130,"props":2445,"children":2446},{"style":143},[2447],{"type":30,"value":436},{"type":25,"tag":130,"props":2449,"children":2450},{"class":132,"line":743},[2451,2455,2459,2464,2468,2473,2477,2482,2487,2491,2495,2499],{"type":25,"tag":130,"props":2452,"children":2453},{"style":137},[2454],{"type":30,"value":1413},{"type":25,"tag":130,"props":2456,"children":2457},{"style":143},[2458],{"type":30,"value":1351},{"type":25,"tag":130,"props":2460,"children":2461},{"style":209},[2462],{"type":30,"value":2463},"this",{"type":25,"tag":130,"props":2465,"children":2466},{"style":143},[2467],{"type":30,"value":2339},{"type":25,"tag":130,"props":2469,"children":2470},{"style":137},[2471],{"type":30,"value":2472},"!==",{"type":25,"tag":130,"props":2474,"children":2475},{"style":209},[2476],{"type":30,"value":2232},{"type":25,"tag":130,"props":2478,"children":2479},{"style":137},[2480],{"type":30,"value":2481}," &&",{"type":25,"tag":130,"props":2483,"children":2484},{"style":209},[2485],{"type":30,"value":2486}," this",{"type":25,"tag":130,"props":2488,"children":2489},{"style":143},[2490],{"type":30,"value":2363},{"type":25,"tag":130,"props":2492,"children":2493},{"style":137},[2494],{"type":30,"value":2472},{"type":25,"tag":130,"props":2496,"children":2497},{"style":209},[2498],{"type":30,"value":2232},{"type":25,"tag":130,"props":2500,"children":2501},{"style":143},[2502],{"type":30,"value":1405},{"type":25,"tag":130,"props":2504,"children":2505},{"class":132,"line":803},[2506,2511,2516,2520,2525,2529,2533,2538,2542,2547],{"type":25,"tag":130,"props":2507,"children":2508},{"style":137},[2509],{"type":30,"value":2510},"      const",{"type":25,"tag":130,"props":2512,"children":2513},{"style":209},[2514],{"type":30,"value":2515}," dtHours",{"type":25,"tag":130,"props":2517,"children":2518},{"style":137},[2519],{"type":30,"value":217},{"type":25,"tag":130,"props":2521,"children":2522},{"style":143},[2523],{"type":30,"value":2524}," (nowMs ",{"type":25,"tag":130,"props":2526,"children":2527},{"style":137},[2528],{"type":30,"value":1200},{"type":25,"tag":130,"props":2530,"children":2531},{"style":209},[2532],{"type":30,"value":2486},{"type":25,"tag":130,"props":2534,"children":2535},{"style":143},[2536],{"type":30,"value":2537},".lastTs) ",{"type":25,"tag":130,"props":2539,"children":2540},{"style":137},[2541],{"type":30,"value":1784},{"type":25,"tag":130,"props":2543,"children":2544},{"style":209},[2545],{"type":30,"value":2546}," 3_600_000",{"type":25,"tag":130,"props":2548,"children":2549},{"style":143},[2550],{"type":30,"value":162},{"type":25,"tag":130,"props":2552,"children":2553},{"class":132,"line":862},[2554,2558,2563,2567,2572,2576,2580,2585,2589,2594],{"type":25,"tag":130,"props":2555,"children":2556},{"style":137},[2557],{"type":30,"value":2510},{"type":25,"tag":130,"props":2559,"children":2560},{"style":209},[2561],{"type":30,"value":2562}," avgW",{"type":25,"tag":130,"props":2564,"children":2565},{"style":137},[2566],{"type":30,"value":217},{"type":25,"tag":130,"props":2568,"children":2569},{"style":143},[2570],{"type":30,"value":2571}," (powerW ",{"type":25,"tag":130,"props":2573,"children":2574},{"style":137},[2575],{"type":30,"value":1446},{"type":25,"tag":130,"props":2577,"children":2578},{"style":209},[2579],{"type":30,"value":2486},{"type":25,"tag":130,"props":2581,"children":2582},{"style":143},[2583],{"type":30,"value":2584},".lastPowerW) ",{"type":25,"tag":130,"props":2586,"children":2587},{"style":137},[2588],{"type":30,"value":1784},{"type":25,"tag":130,"props":2590,"children":2591},{"style":209},[2592],{"type":30,"value":2593}," 2",{"type":25,"tag":130,"props":2595,"children":2596},{"style":143},[2597],{"type":30,"value":162},{"type":25,"tag":130,"props":2599,"children":2600},{"class":132,"line":920},[2601,2606,2610,2615,2620,2625,2630,2634,2639],{"type":25,"tag":130,"props":2602,"children":2603},{"style":209},[2604],{"type":30,"value":2605},"      this",{"type":25,"tag":130,"props":2607,"children":2608},{"style":143},[2609],{"type":30,"value":2318},{"type":25,"tag":130,"props":2611,"children":2612},{"style":137},[2613],{"type":30,"value":2614},"+=",{"type":25,"tag":130,"props":2616,"children":2617},{"style":143},[2618],{"type":30,"value":2619}," (avgW ",{"type":25,"tag":130,"props":2621,"children":2622},{"style":137},[2623],{"type":30,"value":2624},"*",{"type":25,"tag":130,"props":2626,"children":2627},{"style":143},[2628],{"type":30,"value":2629}," dtHours) ",{"type":25,"tag":130,"props":2631,"children":2632},{"style":137},[2633],{"type":30,"value":1784},{"type":25,"tag":130,"props":2635,"children":2636},{"style":209},[2637],{"type":30,"value":2638}," 1000",{"type":25,"tag":130,"props":2640,"children":2641},{"style":143},[2642],{"type":30,"value":162},{"type":25,"tag":130,"props":2644,"children":2645},{"class":132,"line":982},[2646],{"type":25,"tag":130,"props":2647,"children":2648},{"style":143},[2649],{"type":30,"value":1549},{"type":25,"tag":130,"props":2651,"children":2652},{"class":132,"line":992},[2653,2657,2661,2665],{"type":25,"tag":130,"props":2654,"children":2655},{"style":209},[2656],{"type":30,"value":2313},{"type":25,"tag":130,"props":2658,"children":2659},{"style":143},[2660],{"type":30,"value":2339},{"type":25,"tag":130,"props":2662,"children":2663},{"style":137},[2664],{"type":30,"value":606},{"type":25,"tag":130,"props":2666,"children":2667},{"style":143},[2668],{"type":30,"value":2669}," powerW;\n",{"type":25,"tag":130,"props":2671,"children":2672},{"class":132,"line":2055},[2673,2677,2681,2685],{"type":25,"tag":130,"props":2674,"children":2675},{"style":209},[2676],{"type":30,"value":2313},{"type":25,"tag":130,"props":2678,"children":2679},{"style":143},[2680],{"type":30,"value":2363},{"type":25,"tag":130,"props":2682,"children":2683},{"style":137},[2684],{"type":30,"value":606},{"type":25,"tag":130,"props":2686,"children":2687},{"style":143},[2688],{"type":30,"value":2689}," nowMs;\n",{"type":25,"tag":130,"props":2691,"children":2692},{"class":132,"line":2078},[2693,2698,2702],{"type":25,"tag":130,"props":2694,"children":2695},{"style":137},[2696],{"type":30,"value":2697},"    return",{"type":25,"tag":130,"props":2699,"children":2700},{"style":209},[2701],{"type":30,"value":2486},{"type":25,"tag":130,"props":2703,"children":2704},{"style":143},[2705],{"type":30,"value":2706},".totalKwh;\n",{"type":25,"tag":130,"props":2708,"children":2709},{"class":132,"line":2097},[2710],{"type":25,"tag":130,"props":2711,"children":2712},{"style":143},[2713],{"type":30,"value":1557},{"type":25,"tag":130,"props":2715,"children":2716},{"class":132,"line":2106},[2717],{"type":25,"tag":130,"props":2718,"children":2719},{"emptyLinePlaceholder":194},[2720],{"type":30,"value":197},{"type":25,"tag":130,"props":2722,"children":2724},{"class":132,"line":2723},23,[2725,2730,2735,2740,2744,2748],{"type":25,"tag":130,"props":2726,"children":2727},{"style":137},[2728],{"type":30,"value":2729},"  get",{"type":25,"tag":130,"props":2731,"children":2732},{"style":225},[2733],{"type":30,"value":2734}," value",{"type":25,"tag":130,"props":2736,"children":2737},{"style":143},[2738],{"type":30,"value":2739},"()",{"type":25,"tag":130,"props":2741,"children":2742},{"style":137},[2743],{"type":30,"value":450},{"type":25,"tag":130,"props":2745,"children":2746},{"style":209},[2747],{"type":30,"value":455},{"type":25,"tag":130,"props":2749,"children":2750},{"style":143},[2751],{"type":30,"value":436},{"type":25,"tag":130,"props":2753,"children":2755},{"class":132,"line":2754},24,[2756,2760,2764],{"type":25,"tag":130,"props":2757,"children":2758},{"style":137},[2759],{"type":30,"value":2697},{"type":25,"tag":130,"props":2761,"children":2762},{"style":209},[2763],{"type":30,"value":2486},{"type":25,"tag":130,"props":2765,"children":2766},{"style":143},[2767],{"type":30,"value":2706},{"type":25,"tag":130,"props":2769,"children":2771},{"class":132,"line":2770},25,[2772],{"type":25,"tag":130,"props":2773,"children":2774},{"style":143},[2775],{"type":30,"value":1557},{"type":25,"tag":130,"props":2777,"children":2779},{"class":132,"line":2778},26,[2780],{"type":25,"tag":130,"props":2781,"children":2782},{"style":143},[2783],{"type":30,"value":1610},{"type":25,"tag":26,"props":2785,"children":2786},{},[2787],{"type":30,"value":2788},"Accumulators run for: solar generation (PV1 power), load consumption, grid import, and grid export. On container restart, the poller queries TimescaleDB for the current day's accumulated values and restores the accumulators from there. Daily totals survive reboots.",{"type":25,"tag":26,"props":2790,"children":2791},{},[2792,2794,2799],{"type":30,"value":2793},"These publish as ",{"type":25,"tag":69,"props":2795,"children":2797},{"className":2796},[],[2798],{"type":30,"value":2146},{"type":30,"value":2800}," energy sensors, which Home Assistant's Energy Dashboard understands natively. I get daily, weekly, and monthly energy breakdowns with no custom charting code at all.",{"type":25,"tag":78,"props":2802,"children":2804},{"id":2803},"timescaledb-continuous-aggregates-and-retention",[2805],{"type":30,"value":2806},"TimescaleDB: Continuous Aggregates and Retention",{"type":25,"tag":26,"props":2808,"children":2809},{},[2810,2812,2818],{"type":30,"value":2811},"The schema is the same basic approach as before, ",{"type":25,"tag":69,"props":2813,"children":2815},{"className":2814},[],[2816],{"type":30,"value":2817},"(timestamp, key, value)",{"type":30,"value":2819}," rows in a hypertable. The difference is continuous aggregates.",{"type":25,"tag":26,"props":2821,"children":2822},{},[2823],{"type":30,"value":2824},"TimescaleDB can maintain pre-computed rollups that stay current automatically:",{"type":25,"tag":121,"props":2826,"children":2830},{"code":2827,"language":2828,"meta":7,"className":2829,"style":7},"CREATE MATERIALIZED VIEW inverter_hourly\nWITH (timescaledb.continuous) AS\nSELECT\n  time_bucket('1 hour', ts) AS bucket,\n  key,\n  avg(value)  AS avg_val,\n  min(value)  AS min_val,\n  max(value)  AS max_val\nFROM inverter_metrics\nGROUP BY bucket, key;\n\nSELECT add_continuous_aggregate_policy('inverter_hourly',\n  start_offset => INTERVAL '3 hours',\n  end_offset   => INTERVAL '1 hour',\n  schedule_interval => INTERVAL '1 hour'\n);\n","sql","language-sql shiki shiki-themes github-dark",[2831],{"type":25,"tag":69,"props":2832,"children":2833},{"__ignoreMap":7},[2834,2842,2850,2858,2866,2874,2882,2890,2898,2906,2914,2921,2929,2937,2945,2953],{"type":25,"tag":130,"props":2835,"children":2836},{"class":132,"line":133},[2837],{"type":25,"tag":130,"props":2838,"children":2839},{},[2840],{"type":30,"value":2841},"CREATE MATERIALIZED VIEW inverter_hourly\n",{"type":25,"tag":130,"props":2843,"children":2844},{"class":132,"line":19},[2845],{"type":25,"tag":130,"props":2846,"children":2847},{},[2848],{"type":30,"value":2849},"WITH (timescaledb.continuous) AS\n",{"type":25,"tag":130,"props":2851,"children":2852},{"class":132,"line":190},[2853],{"type":25,"tag":130,"props":2854,"children":2855},{},[2856],{"type":30,"value":2857},"SELECT\n",{"type":25,"tag":130,"props":2859,"children":2860},{"class":132,"line":200},[2861],{"type":25,"tag":130,"props":2862,"children":2863},{},[2864],{"type":30,"value":2865},"  time_bucket('1 hour', ts) AS bucket,\n",{"type":25,"tag":130,"props":2867,"children":2868},{"class":132,"line":236},[2869],{"type":25,"tag":130,"props":2870,"children":2871},{},[2872],{"type":30,"value":2873},"  key,\n",{"type":25,"tag":130,"props":2875,"children":2876},{"class":132,"line":270},[2877],{"type":25,"tag":130,"props":2878,"children":2879},{},[2880],{"type":30,"value":2881},"  avg(value)  AS avg_val,\n",{"type":25,"tag":130,"props":2883,"children":2884},{"class":132,"line":289},[2885],{"type":25,"tag":130,"props":2886,"children":2887},{},[2888],{"type":30,"value":2889},"  min(value)  AS min_val,\n",{"type":25,"tag":130,"props":2891,"children":2892},{"class":132,"line":307},[2893],{"type":25,"tag":130,"props":2894,"children":2895},{},[2896],{"type":30,"value":2897},"  max(value)  AS max_val\n",{"type":25,"tag":130,"props":2899,"children":2900},{"class":132,"line":325},[2901],{"type":25,"tag":130,"props":2902,"children":2903},{},[2904],{"type":30,"value":2905},"FROM inverter_metrics\n",{"type":25,"tag":130,"props":2907,"children":2908},{"class":132,"line":343},[2909],{"type":25,"tag":130,"props":2910,"children":2911},{},[2912],{"type":30,"value":2913},"GROUP BY bucket, key;\n",{"type":25,"tag":130,"props":2915,"children":2916},{"class":132,"line":352},[2917],{"type":25,"tag":130,"props":2918,"children":2919},{"emptyLinePlaceholder":194},[2920],{"type":30,"value":197},{"type":25,"tag":130,"props":2922,"children":2923},{"class":132,"line":379},[2924],{"type":25,"tag":130,"props":2925,"children":2926},{},[2927],{"type":30,"value":2928},"SELECT add_continuous_aggregate_policy('inverter_hourly',\n",{"type":25,"tag":130,"props":2930,"children":2931},{"class":132,"line":743},[2932],{"type":25,"tag":130,"props":2933,"children":2934},{},[2935],{"type":30,"value":2936},"  start_offset => INTERVAL '3 hours',\n",{"type":25,"tag":130,"props":2938,"children":2939},{"class":132,"line":803},[2940],{"type":25,"tag":130,"props":2941,"children":2942},{},[2943],{"type":30,"value":2944},"  end_offset   => INTERVAL '1 hour',\n",{"type":25,"tag":130,"props":2946,"children":2947},{"class":132,"line":862},[2948],{"type":25,"tag":130,"props":2949,"children":2950},{},[2951],{"type":30,"value":2952},"  schedule_interval => INTERVAL '1 hour'\n",{"type":25,"tag":130,"props":2954,"children":2955},{"class":132,"line":920},[2956],{"type":25,"tag":130,"props":2957,"children":2958},{},[2959],{"type":30,"value":376},{"type":25,"tag":26,"props":2961,"children":2962},{},[2963,2965,2971],{"type":30,"value":2964},"Daily aggregates use ",{"type":25,"tag":69,"props":2966,"children":2968},{"className":2967},[],[2969],{"type":30,"value":2970},"time_bucket_gapfill",{"type":30,"value":2972}," with timezone offset so buckets align to local midnight rather than UTC midnight. This matters when you want to show \"today's solar yield\" and you're not on UTC.",{"type":25,"tag":26,"props":2974,"children":2975},{},[2976],{"type":30,"value":2977},"Raw data has a 30-day retention policy. The continuous aggregates keep hourly and daily summaries indefinitely. This keeps the database small while preserving the data that actually gets queried.",{"type":25,"tag":78,"props":2979,"children":2981},{"id":2980},"the-full-stack-in-docker-compose",[2982],{"type":30,"value":2983},"The Full Stack in Docker Compose",{"type":25,"tag":26,"props":2985,"children":2986},{},[2987],{"type":30,"value":2988},"Everything runs in a single Compose file:",{"type":25,"tag":121,"props":2990,"children":2994},{"code":2991,"language":2992,"meta":7,"className":2993,"style":7},"services:\n  mosquitto:\n    image: eclipse-mosquitto:2\n    volumes:\n      - ./mosquitto/config:/mosquitto/config\n      - mosquitto-data:/mosquitto/data\n    ports:\n      - \"1883:1883\"\n\n  timescaledb:\n    image: timescale/timescaledb:latest-pg16\n    environment:\n      POSTGRES_DB: solar\n      POSTGRES_USER: solar\n      POSTGRES_PASSWORD: ${DB_PASSWORD}\n    volumes:\n      - tsdb-data:/var/lib/postgresql/data\n      - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql\n\n  poller:\n    build: ./poller\n    depends_on:\n      - mosquitto\n      - timescaledb\n    environment:\n      MQTT_HOST: mosquitto\n      DB_HOST: timescaledb\n      DB_PASSWORD: ${DB_PASSWORD}\n      TZ: ${TZ}\n    devices:\n      - /dev/ttyUSB0:/dev/ttyUSB0\n    restart: unless-stopped\n\nvolumes:\n  mosquitto-data:\n  tsdb-data:\n","yaml","language-yaml shiki shiki-themes github-dark",[2995],{"type":25,"tag":69,"props":2996,"children":2997},{"__ignoreMap":7},[2998,3012,3024,3042,3054,3067,3079,3091,3103,3110,3122,3138,3150,3167,3183,3200,3211,3223,3235,3242,3254,3271,3283,3295,3307,3318,3334,3351,3368,3386,3399,3412,3430,3438,3451,3464],{"type":25,"tag":130,"props":2999,"children":3000},{"class":132,"line":133},[3001,3007],{"type":25,"tag":130,"props":3002,"children":3004},{"style":3003},"--shiki-default:#85E89D",[3005],{"type":30,"value":3006},"services",{"type":25,"tag":130,"props":3008,"children":3009},{"style":143},[3010],{"type":30,"value":3011},":\n",{"type":25,"tag":130,"props":3013,"children":3014},{"class":132,"line":19},[3015,3020],{"type":25,"tag":130,"props":3016,"children":3017},{"style":3003},[3018],{"type":30,"value":3019},"  mosquitto",{"type":25,"tag":130,"props":3021,"children":3022},{"style":143},[3023],{"type":30,"value":3011},{"type":25,"tag":130,"props":3025,"children":3026},{"class":132,"line":190},[3027,3032,3037],{"type":25,"tag":130,"props":3028,"children":3029},{"style":3003},[3030],{"type":30,"value":3031},"    image",{"type":25,"tag":130,"props":3033,"children":3034},{"style":143},[3035],{"type":30,"value":3036},": ",{"type":25,"tag":130,"props":3038,"children":3039},{"style":154},[3040],{"type":30,"value":3041},"eclipse-mosquitto:2\n",{"type":25,"tag":130,"props":3043,"children":3044},{"class":132,"line":200},[3045,3050],{"type":25,"tag":130,"props":3046,"children":3047},{"style":3003},[3048],{"type":30,"value":3049},"    volumes",{"type":25,"tag":130,"props":3051,"children":3052},{"style":143},[3053],{"type":30,"value":3011},{"type":25,"tag":130,"props":3055,"children":3056},{"class":132,"line":236},[3057,3062],{"type":25,"tag":130,"props":3058,"children":3059},{"style":143},[3060],{"type":30,"value":3061},"      - ",{"type":25,"tag":130,"props":3063,"children":3064},{"style":154},[3065],{"type":30,"value":3066},"./mosquitto/config:/mosquitto/config\n",{"type":25,"tag":130,"props":3068,"children":3069},{"class":132,"line":270},[3070,3074],{"type":25,"tag":130,"props":3071,"children":3072},{"style":143},[3073],{"type":30,"value":3061},{"type":25,"tag":130,"props":3075,"children":3076},{"style":154},[3077],{"type":30,"value":3078},"mosquitto-data:/mosquitto/data\n",{"type":25,"tag":130,"props":3080,"children":3081},{"class":132,"line":289},[3082,3087],{"type":25,"tag":130,"props":3083,"children":3084},{"style":3003},[3085],{"type":30,"value":3086},"    ports",{"type":25,"tag":130,"props":3088,"children":3089},{"style":143},[3090],{"type":30,"value":3011},{"type":25,"tag":130,"props":3092,"children":3093},{"class":132,"line":307},[3094,3098],{"type":25,"tag":130,"props":3095,"children":3096},{"style":143},[3097],{"type":30,"value":3061},{"type":25,"tag":130,"props":3099,"children":3100},{"style":154},[3101],{"type":30,"value":3102},"\"1883:1883\"\n",{"type":25,"tag":130,"props":3104,"children":3105},{"class":132,"line":325},[3106],{"type":25,"tag":130,"props":3107,"children":3108},{"emptyLinePlaceholder":194},[3109],{"type":30,"value":197},{"type":25,"tag":130,"props":3111,"children":3112},{"class":132,"line":343},[3113,3118],{"type":25,"tag":130,"props":3114,"children":3115},{"style":3003},[3116],{"type":30,"value":3117},"  timescaledb",{"type":25,"tag":130,"props":3119,"children":3120},{"style":143},[3121],{"type":30,"value":3011},{"type":25,"tag":130,"props":3123,"children":3124},{"class":132,"line":352},[3125,3129,3133],{"type":25,"tag":130,"props":3126,"children":3127},{"style":3003},[3128],{"type":30,"value":3031},{"type":25,"tag":130,"props":3130,"children":3131},{"style":143},[3132],{"type":30,"value":3036},{"type":25,"tag":130,"props":3134,"children":3135},{"style":154},[3136],{"type":30,"value":3137},"timescale/timescaledb:latest-pg16\n",{"type":25,"tag":130,"props":3139,"children":3140},{"class":132,"line":379},[3141,3146],{"type":25,"tag":130,"props":3142,"children":3143},{"style":3003},[3144],{"type":30,"value":3145},"    environment",{"type":25,"tag":130,"props":3147,"children":3148},{"style":143},[3149],{"type":30,"value":3011},{"type":25,"tag":130,"props":3151,"children":3152},{"class":132,"line":743},[3153,3158,3162],{"type":25,"tag":130,"props":3154,"children":3155},{"style":3003},[3156],{"type":30,"value":3157},"      POSTGRES_DB",{"type":25,"tag":130,"props":3159,"children":3160},{"style":143},[3161],{"type":30,"value":3036},{"type":25,"tag":130,"props":3163,"children":3164},{"style":154},[3165],{"type":30,"value":3166},"solar\n",{"type":25,"tag":130,"props":3168,"children":3169},{"class":132,"line":803},[3170,3175,3179],{"type":25,"tag":130,"props":3171,"children":3172},{"style":3003},[3173],{"type":30,"value":3174},"      POSTGRES_USER",{"type":25,"tag":130,"props":3176,"children":3177},{"style":143},[3178],{"type":30,"value":3036},{"type":25,"tag":130,"props":3180,"children":3181},{"style":154},[3182],{"type":30,"value":3166},{"type":25,"tag":130,"props":3184,"children":3185},{"class":132,"line":862},[3186,3191,3195],{"type":25,"tag":130,"props":3187,"children":3188},{"style":3003},[3189],{"type":30,"value":3190},"      POSTGRES_PASSWORD",{"type":25,"tag":130,"props":3192,"children":3193},{"style":143},[3194],{"type":30,"value":3036},{"type":25,"tag":130,"props":3196,"children":3197},{"style":154},[3198],{"type":30,"value":3199},"${DB_PASSWORD}\n",{"type":25,"tag":130,"props":3201,"children":3202},{"class":132,"line":920},[3203,3207],{"type":25,"tag":130,"props":3204,"children":3205},{"style":3003},[3206],{"type":30,"value":3049},{"type":25,"tag":130,"props":3208,"children":3209},{"style":143},[3210],{"type":30,"value":3011},{"type":25,"tag":130,"props":3212,"children":3213},{"class":132,"line":982},[3214,3218],{"type":25,"tag":130,"props":3215,"children":3216},{"style":143},[3217],{"type":30,"value":3061},{"type":25,"tag":130,"props":3219,"children":3220},{"style":154},[3221],{"type":30,"value":3222},"tsdb-data:/var/lib/postgresql/data\n",{"type":25,"tag":130,"props":3224,"children":3225},{"class":132,"line":992},[3226,3230],{"type":25,"tag":130,"props":3227,"children":3228},{"style":143},[3229],{"type":30,"value":3061},{"type":25,"tag":130,"props":3231,"children":3232},{"style":154},[3233],{"type":30,"value":3234},"./sql/init.sql:/docker-entrypoint-initdb.d/init.sql\n",{"type":25,"tag":130,"props":3236,"children":3237},{"class":132,"line":2055},[3238],{"type":25,"tag":130,"props":3239,"children":3240},{"emptyLinePlaceholder":194},[3241],{"type":30,"value":197},{"type":25,"tag":130,"props":3243,"children":3244},{"class":132,"line":2078},[3245,3250],{"type":25,"tag":130,"props":3246,"children":3247},{"style":3003},[3248],{"type":30,"value":3249},"  poller",{"type":25,"tag":130,"props":3251,"children":3252},{"style":143},[3253],{"type":30,"value":3011},{"type":25,"tag":130,"props":3255,"children":3256},{"class":132,"line":2097},[3257,3262,3266],{"type":25,"tag":130,"props":3258,"children":3259},{"style":3003},[3260],{"type":30,"value":3261},"    build",{"type":25,"tag":130,"props":3263,"children":3264},{"style":143},[3265],{"type":30,"value":3036},{"type":25,"tag":130,"props":3267,"children":3268},{"style":154},[3269],{"type":30,"value":3270},"./poller\n",{"type":25,"tag":130,"props":3272,"children":3273},{"class":132,"line":2106},[3274,3279],{"type":25,"tag":130,"props":3275,"children":3276},{"style":3003},[3277],{"type":30,"value":3278},"    depends_on",{"type":25,"tag":130,"props":3280,"children":3281},{"style":143},[3282],{"type":30,"value":3011},{"type":25,"tag":130,"props":3284,"children":3285},{"class":132,"line":2723},[3286,3290],{"type":25,"tag":130,"props":3287,"children":3288},{"style":143},[3289],{"type":30,"value":3061},{"type":25,"tag":130,"props":3291,"children":3292},{"style":154},[3293],{"type":30,"value":3294},"mosquitto\n",{"type":25,"tag":130,"props":3296,"children":3297},{"class":132,"line":2754},[3298,3302],{"type":25,"tag":130,"props":3299,"children":3300},{"style":143},[3301],{"type":30,"value":3061},{"type":25,"tag":130,"props":3303,"children":3304},{"style":154},[3305],{"type":30,"value":3306},"timescaledb\n",{"type":25,"tag":130,"props":3308,"children":3309},{"class":132,"line":2770},[3310,3314],{"type":25,"tag":130,"props":3311,"children":3312},{"style":3003},[3313],{"type":30,"value":3145},{"type":25,"tag":130,"props":3315,"children":3316},{"style":143},[3317],{"type":30,"value":3011},{"type":25,"tag":130,"props":3319,"children":3320},{"class":132,"line":2778},[3321,3326,3330],{"type":25,"tag":130,"props":3322,"children":3323},{"style":3003},[3324],{"type":30,"value":3325},"      MQTT_HOST",{"type":25,"tag":130,"props":3327,"children":3328},{"style":143},[3329],{"type":30,"value":3036},{"type":25,"tag":130,"props":3331,"children":3332},{"style":154},[3333],{"type":30,"value":3294},{"type":25,"tag":130,"props":3335,"children":3337},{"class":132,"line":3336},27,[3338,3343,3347],{"type":25,"tag":130,"props":3339,"children":3340},{"style":3003},[3341],{"type":30,"value":3342},"      DB_HOST",{"type":25,"tag":130,"props":3344,"children":3345},{"style":143},[3346],{"type":30,"value":3036},{"type":25,"tag":130,"props":3348,"children":3349},{"style":154},[3350],{"type":30,"value":3306},{"type":25,"tag":130,"props":3352,"children":3354},{"class":132,"line":3353},28,[3355,3360,3364],{"type":25,"tag":130,"props":3356,"children":3357},{"style":3003},[3358],{"type":30,"value":3359},"      DB_PASSWORD",{"type":25,"tag":130,"props":3361,"children":3362},{"style":143},[3363],{"type":30,"value":3036},{"type":25,"tag":130,"props":3365,"children":3366},{"style":154},[3367],{"type":30,"value":3199},{"type":25,"tag":130,"props":3369,"children":3371},{"class":132,"line":3370},29,[3372,3377,3381],{"type":25,"tag":130,"props":3373,"children":3374},{"style":3003},[3375],{"type":30,"value":3376},"      TZ",{"type":25,"tag":130,"props":3378,"children":3379},{"style":143},[3380],{"type":30,"value":3036},{"type":25,"tag":130,"props":3382,"children":3383},{"style":154},[3384],{"type":30,"value":3385},"${TZ}\n",{"type":25,"tag":130,"props":3387,"children":3389},{"class":132,"line":3388},30,[3390,3395],{"type":25,"tag":130,"props":3391,"children":3392},{"style":3003},[3393],{"type":30,"value":3394},"    devices",{"type":25,"tag":130,"props":3396,"children":3397},{"style":143},[3398],{"type":30,"value":3011},{"type":25,"tag":130,"props":3400,"children":3402},{"class":132,"line":3401},31,[3403,3407],{"type":25,"tag":130,"props":3404,"children":3405},{"style":143},[3406],{"type":30,"value":3061},{"type":25,"tag":130,"props":3408,"children":3409},{"style":154},[3410],{"type":30,"value":3411},"/dev/ttyUSB0:/dev/ttyUSB0\n",{"type":25,"tag":130,"props":3413,"children":3415},{"class":132,"line":3414},32,[3416,3421,3425],{"type":25,"tag":130,"props":3417,"children":3418},{"style":3003},[3419],{"type":30,"value":3420},"    restart",{"type":25,"tag":130,"props":3422,"children":3423},{"style":143},[3424],{"type":30,"value":3036},{"type":25,"tag":130,"props":3426,"children":3427},{"style":154},[3428],{"type":30,"value":3429},"unless-stopped\n",{"type":25,"tag":130,"props":3431,"children":3433},{"class":132,"line":3432},33,[3434],{"type":25,"tag":130,"props":3435,"children":3436},{"emptyLinePlaceholder":194},[3437],{"type":30,"value":197},{"type":25,"tag":130,"props":3439,"children":3441},{"class":132,"line":3440},34,[3442,3447],{"type":25,"tag":130,"props":3443,"children":3444},{"style":3003},[3445],{"type":30,"value":3446},"volumes",{"type":25,"tag":130,"props":3448,"children":3449},{"style":143},[3450],{"type":30,"value":3011},{"type":25,"tag":130,"props":3452,"children":3454},{"class":132,"line":3453},35,[3455,3460],{"type":25,"tag":130,"props":3456,"children":3457},{"style":3003},[3458],{"type":30,"value":3459},"  mosquitto-data",{"type":25,"tag":130,"props":3461,"children":3462},{"style":143},[3463],{"type":30,"value":3011},{"type":25,"tag":130,"props":3465,"children":3467},{"class":132,"line":3466},36,[3468,3473],{"type":25,"tag":130,"props":3469,"children":3470},{"style":3003},[3471],{"type":30,"value":3472},"  tsdb-data",{"type":25,"tag":130,"props":3474,"children":3475},{"style":143},[3476],{"type":30,"value":3011},{"type":25,"tag":26,"props":3478,"children":3479},{},[3480,3482,3488],{"type":30,"value":3481},"A ",{"type":25,"tag":69,"props":3483,"children":3485},{"className":3484},[],[3486],{"type":30,"value":3487},".env",{"type":30,"value":3489}," file carries the timezone and any secrets. The whole stack rebuilds from scratch in under a minute on a fresh machine.",{"type":25,"tag":26,"props":3491,"children":3492},{},[3493,3495,3501],{"type":30,"value":3494},"The Mosquitto broker is also shared with other Docker Compose stacks on the same host via an external Docker network. Solar metrics, mining stats, and general homelab monitoring all flow through one broker. Any service that wants solar data subscribes to ",{"type":25,"tag":69,"props":3496,"children":3498},{"className":3497},[],[3499],{"type":30,"value":3500},"solar/#",{"type":30,"value":3502},". The poller has no idea who's listening.",{"type":25,"tag":78,"props":3504,"children":3506},{"id":3505},"what-got-better",[3507],{"type":30,"value":3508},"What Got Better",{"type":25,"tag":26,"props":3510,"children":3511},{},[3512,3518],{"type":25,"tag":3513,"props":3514,"children":3515},"strong",{},[3516],{"type":30,"value":3517},"No more custom dashboard.",{"type":30,"value":3519}," The Home Assistant Energy Dashboard gives daily and monthly breakdowns out of the box. Lovelace cards give me live gauges and power flow visualization. All of it is maintained by the HA community, not by me.",{"type":25,"tag":26,"props":3521,"children":3522},{},[3523,3528,3530,3535],{"type":25,"tag":3513,"props":3524,"children":3525},{},[3526],{"type":30,"value":3527},"Adding a metric takes three lines.",{"type":30,"value":3529}," Drop a new entry in the ",{"type":25,"tag":69,"props":3531,"children":3533},{"className":3532},[],[3534],{"type":30,"value":2127},{"type":30,"value":3536}," array with the address, name, scale, and unit. TypeScript compilation catches mistakes. The sensor appears in Home Assistant on the next deploy.",{"type":25,"tag":26,"props":3538,"children":3539},{},[3540,3545,3547,3553],{"type":25,"tag":3513,"props":3541,"children":3542},{},[3543],{"type":30,"value":3544},"The whole stack fits in a repository.",{"type":30,"value":3546}," ",{"type":25,"tag":69,"props":3548,"children":3550},{"className":3549},[],[3551],{"type":30,"value":3552},"docker compose up -d",{"type":30,"value":3554}," on a new machine and everything is back. That lesson cost me twice before I learned it.",{"type":25,"tag":26,"props":3556,"children":3557},{},[3558,3563],{"type":25,"tag":3513,"props":3559,"children":3560},{},[3561],{"type":30,"value":3562},"Energy tracking works.",{"type":30,"value":3564}," The trapezoidal accumulators and TimescaleDB continuous aggregates give me accurate daily kWh numbers without writing any aggregation code in the application layer.",{"type":25,"tag":78,"props":3566,"children":3568},{"id":3567},"what-i-learned",[3569],{"type":30,"value":3570},"What I Learned",{"type":25,"tag":26,"props":3572,"children":3573},{},[3574],{"type":30,"value":3575},"The biggest lesson isn't about MQTT or TypeScript. It's that a custom dashboard is a liability, not just an asset. Building it was satisfying. Maintaining it was a cost I was paying every time I wanted to change anything. Handing that responsibility to Home Assistant removed the biggest maintenance surface from the whole stack.",{"type":25,"tag":26,"props":3577,"children":3578},{},[3579],{"type":30,"value":3580},"MQTT as an integration layer is genuinely more flexible than a custom REST API. Anything can subscribe. Multiple consumers can get the same data without the poller knowing about them. New integrations (future me wants InfluxDB, apparently) just subscribe to the existing topics.",{"type":25,"tag":26,"props":3582,"children":3583},{},[3584],{"type":30,"value":3585},"TimescaleDB continuous aggregates are one of the more underrated features in any database I've used. I write 5-second raw data and hourly and daily rollups maintain themselves. No cron jobs, no application-level aggregation, no stale numbers if the poller restarts.",{"type":25,"tag":26,"props":3587,"children":3588},{},[3589],{"type":30,"value":3590},"And the thing that should have been obvious: push your code to a repository. Not someday. Before you deploy it.",{"type":25,"tag":42,"props":3592,"children":3593},{},[],{"type":25,"tag":26,"props":3595,"children":3596},{},[3597],{"type":30,"value":3598},"The solar setup is running the new stack right now. If you want to build something similar, the architecture is straightforward. Modbus RTU over USB, 5-second polling, MQTT for distribution, Home Assistant for visualization, TimescaleDB for history. None of it requires specialized hardware or expensive software.",{"type":25,"tag":26,"props":3600,"children":3601},{},[3602],{"type":30,"value":3603},"If you have the same inverter or a similar Modbus-speaking one, drop a comment. The register mapping is the hardest part, and the more documentation exists in public, the less time anyone spends reverse-engineering the same spec.",{"type":25,"tag":3605,"props":3606,"children":3607},"style",{},[3608],{"type":30,"value":3609},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":7,"searchDepth":19,"depth":19,"links":3611},[3612,3613,3616,3617,3618,3619,3620,3621,3622],{"id":80,"depth":19,"text":83},{"id":96,"depth":19,"text":99,"children":3614},[3615],{"id":1007,"depth":190,"text":1010},{"id":1618,"depth":19,"text":1621},{"id":1647,"depth":19,"text":1650},{"id":2132,"depth":19,"text":2135},{"id":2803,"depth":19,"text":2806},{"id":2980,"depth":19,"text":2983},{"id":3505,"depth":19,"text":3508},{"id":3567,"depth":19,"text":3570},"markdown","content:blog:solar-monitoring-part-2-the-typescript-rebuild.md","content","blog/solar-monitoring-part-2-the-typescript-rebuild.md","blog/solar-monitoring-part-2-the-typescript-rebuild","md",1774200455342]