[{"data":1,"prerenderedAt":5560},["ShallowReactive",2],{"articles-tag-homelab":3},[4,1047,1802,5336],{"_path":5,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":9,"description":10,"date":11,"updated":11,"tags":12,"readingTime":18,"cover":8,"body":19,"_type":1041,"_id":1042,"_source":1043,"_file":1044,"_stem":1045,"_extension":1046},"/blog/home-assistant-mqtt-and-the-timezone-confusion-i-made-myself","blog",false,"","Home Assistant, MQTT That Clicked, and Two Timezone Bugs","Returning to Home Assistant after nearly a decade away, getting solar monitoring back online with MQTT, and debugging two timezone issues I introduced.","2026-03-09",[13,14,15,16,17],"home-assistant","mqtt","homelab","solar-tracker","timescaledb","7 min read",{"type":20,"children":21,"toc":1032},"root",[22,30,35,42,56,77,83,88,93,99,104,119,124,144,149,377,382,395,401,406,411,416,422,427,432,437,450,531,536,542,547,573,578,627,662,794,845,871,891,943,948,992,1005,1011,1016,1021,1026],{"type":23,"tag":24,"props":25,"children":26},"element","p",{},[27],{"type":28,"value":29},"text","About eight or nine years ago, I tried to set up Home Assistant as a Z-Wave hub. I don't remember the details of what went wrong, only that it went wrong, and I walked away frustrated and stuck with a Vera hub I've been using ever since. The UI on the Vera is bad. It has always been bad. I kept it because I didn't want to go through the setup process again.",{"type":23,"tag":24,"props":31,"children":32},{},[33],{"type":28,"value":34},"That changed this past weekend.",{"type":23,"tag":36,"props":37,"children":39},"h2",{"id":38},"why-i-came-back",[40],{"type":28,"value":41},"Why I Came Back",{"type":23,"tag":24,"props":43,"children":44},{},[45,47,54],{"type":28,"value":46},"The short version is solar monitoring. My Raspberry Pi running Solar Assistant died a while back, and then, as I covered ",{"type":23,"tag":48,"props":49,"children":51},"a",{"href":50},"/blog/well-i-embarrassed-myself-even-sooner-than-expected-a-modular-psu-cables-tale",[52],{"type":28,"value":53},"in my last post",{"type":28,"value":55},", I made the situation worse by frying an SSD with the wrong PSU cables. Rather than rebuild the old setup from scratch, a friend pushed me toward Home Assistant. I had been putting it off for obvious reasons.",{"type":23,"tag":24,"props":57,"children":58},{},[59,61,67,69,75],{"type":28,"value":60},"The longer version is in ",{"type":23,"tag":48,"props":62,"children":64},{"href":63},"/blog/solar-monitoring-part-1-the-python-build",[65],{"type":28,"value":66},"part one",{"type":28,"value":68}," and ",{"type":23,"tag":48,"props":70,"children":72},{"href":71},"/blog/solar-monitoring-part-2-the-typescript-rebuild",[73],{"type":28,"value":74},"part two",{"type":28,"value":76}," of the solar monitoring series, where I go into the full stack rebuild. For this post I want to focus on the Home Assistant experience itself, specifically two parts of it: MQTT, and the timezone confusion that made my dashboards look completely wrong for the better part of a day.",{"type":23,"tag":36,"props":78,"children":80},{"id":79},"the-setup-wasnt-what-i-expected",[81],{"type":28,"value":82},"The Setup Wasn't What I Expected",{"type":23,"tag":24,"props":84,"children":85},{},[86],{"type":28,"value":87},"I set Home Assistant up on an existing machine over the weekend. Within a day I had it running, my solar poller reconnected, and data flowing through an MQTT broker into Home Assistant sensors. A day. The last time I attempted this it took longer than that to decide I was done.",{"type":23,"tag":24,"props":89,"children":90},{},[91],{"type":28,"value":92},"Home Assistant has changed a lot in the years I wasn't paying attention. The onboarding is clean. The integrations catalog is massive. The built-in Energy Dashboard has features I would have spent weeks building by hand previously. I'm still figuring out some of it, but the barrier to getting something useful running is genuinely low now.",{"type":23,"tag":36,"props":94,"children":96},{"id":95},"mqtt-finally-made-sense",[97],{"type":28,"value":98},"MQTT Finally Made Sense",{"type":23,"tag":24,"props":100,"children":101},{},[102],{"type":28,"value":103},"MQTT is a protocol I had seen the name of for years without ever finding the time to actually look into. Publish-subscribe message bus, sensors push values, consumers listen to topics. That was roughly where my knowledge stopped.",{"type":23,"tag":24,"props":105,"children":106},{},[107,109,117],{"type":28,"value":108},"Home Assistant changes that dynamic. When you install the Mosquitto broker add-on and connect the MQTT integration, the ",{"type":23,"tag":48,"props":110,"children":114},{"href":111,"rel":112},"https://www.home-assistant.io/integrations/mqtt/#mqtt-discovery",[113],"nofollow",[115],{"type":28,"value":116},"MQTT auto-discovery",{"type":28,"value":118}," mechanism does most of the work. Any device or service that publishes a configuration payload to the right topic becomes a Home Assistant entity automatically. No YAML. No manual entity setup. The device announces itself, Home Assistant listens, the sensor appears.",{"type":23,"tag":24,"props":120,"children":121},{},[122],{"type":28,"value":123},"The discovery topic pattern looks like this:",{"type":23,"tag":125,"props":126,"children":129},"pre",{"className":127,"code":128,"language":28,"meta":8,"style":8},"language-text shiki shiki-themes github-dark","homeassistant/sensor/\u003Cdevice-id>/\u003Cmetric>/config\n",[130],{"type":23,"tag":131,"props":132,"children":133},"code",{"__ignoreMap":8},[134],{"type":23,"tag":135,"props":136,"children":139},"span",{"class":137,"line":138},"line",1,[140],{"type":23,"tag":135,"props":141,"children":142},{},[143],{"type":28,"value":128},{"type":23,"tag":24,"props":145,"children":146},{},[147],{"type":28,"value":148},"The payload is a JSON object describing the sensor, including its name, state topic, unit of measurement, and device class. Once that message is retained on the broker, the entity exists in Home Assistant and updates any time a new value is published to the state topic.",{"type":23,"tag":125,"props":150,"children":154},{"className":151,"code":152,"language":153,"meta":8,"style":8},"language-json shiki shiki-themes github-dark","{\n  \"name\": \"battery_soc\",\n  \"state_topic\": \"solar/battery_soc\",\n  \"unit_of_measurement\": \"%\",\n  \"device_class\": \"battery\",\n  \"state_class\": \"measurement\",\n  \"unique_id\": \"sph5048_battery_soc\",\n  \"device\": {\n    \"identifiers\": [\"sph5048\"],\n    \"name\": \"Solar Inverter\"\n  }\n}\n","json",[155],{"type":23,"tag":131,"props":156,"children":157},{"__ignoreMap":8},[158,167,193,215,237,259,281,303,317,341,359,368],{"type":23,"tag":135,"props":159,"children":160},{"class":137,"line":138},[161],{"type":23,"tag":135,"props":162,"children":164},{"style":163},"--shiki-default:#E1E4E8",[165],{"type":28,"value":166},"{\n",{"type":23,"tag":135,"props":168,"children":170},{"class":137,"line":169},2,[171,177,182,188],{"type":23,"tag":135,"props":172,"children":174},{"style":173},"--shiki-default:#79B8FF",[175],{"type":28,"value":176},"  \"name\"",{"type":23,"tag":135,"props":178,"children":179},{"style":163},[180],{"type":28,"value":181},": ",{"type":23,"tag":135,"props":183,"children":185},{"style":184},"--shiki-default:#9ECBFF",[186],{"type":28,"value":187},"\"battery_soc\"",{"type":23,"tag":135,"props":189,"children":190},{"style":163},[191],{"type":28,"value":192},",\n",{"type":23,"tag":135,"props":194,"children":196},{"class":137,"line":195},3,[197,202,206,211],{"type":23,"tag":135,"props":198,"children":199},{"style":173},[200],{"type":28,"value":201},"  \"state_topic\"",{"type":23,"tag":135,"props":203,"children":204},{"style":163},[205],{"type":28,"value":181},{"type":23,"tag":135,"props":207,"children":208},{"style":184},[209],{"type":28,"value":210},"\"solar/battery_soc\"",{"type":23,"tag":135,"props":212,"children":213},{"style":163},[214],{"type":28,"value":192},{"type":23,"tag":135,"props":216,"children":218},{"class":137,"line":217},4,[219,224,228,233],{"type":23,"tag":135,"props":220,"children":221},{"style":173},[222],{"type":28,"value":223},"  \"unit_of_measurement\"",{"type":23,"tag":135,"props":225,"children":226},{"style":163},[227],{"type":28,"value":181},{"type":23,"tag":135,"props":229,"children":230},{"style":184},[231],{"type":28,"value":232},"\"%\"",{"type":23,"tag":135,"props":234,"children":235},{"style":163},[236],{"type":28,"value":192},{"type":23,"tag":135,"props":238,"children":240},{"class":137,"line":239},5,[241,246,250,255],{"type":23,"tag":135,"props":242,"children":243},{"style":173},[244],{"type":28,"value":245},"  \"device_class\"",{"type":23,"tag":135,"props":247,"children":248},{"style":163},[249],{"type":28,"value":181},{"type":23,"tag":135,"props":251,"children":252},{"style":184},[253],{"type":28,"value":254},"\"battery\"",{"type":23,"tag":135,"props":256,"children":257},{"style":163},[258],{"type":28,"value":192},{"type":23,"tag":135,"props":260,"children":262},{"class":137,"line":261},6,[263,268,272,277],{"type":23,"tag":135,"props":264,"children":265},{"style":173},[266],{"type":28,"value":267},"  \"state_class\"",{"type":23,"tag":135,"props":269,"children":270},{"style":163},[271],{"type":28,"value":181},{"type":23,"tag":135,"props":273,"children":274},{"style":184},[275],{"type":28,"value":276},"\"measurement\"",{"type":23,"tag":135,"props":278,"children":279},{"style":163},[280],{"type":28,"value":192},{"type":23,"tag":135,"props":282,"children":284},{"class":137,"line":283},7,[285,290,294,299],{"type":23,"tag":135,"props":286,"children":287},{"style":173},[288],{"type":28,"value":289},"  \"unique_id\"",{"type":23,"tag":135,"props":291,"children":292},{"style":163},[293],{"type":28,"value":181},{"type":23,"tag":135,"props":295,"children":296},{"style":184},[297],{"type":28,"value":298},"\"sph5048_battery_soc\"",{"type":23,"tag":135,"props":300,"children":301},{"style":163},[302],{"type":28,"value":192},{"type":23,"tag":135,"props":304,"children":306},{"class":137,"line":305},8,[307,312],{"type":23,"tag":135,"props":308,"children":309},{"style":173},[310],{"type":28,"value":311},"  \"device\"",{"type":23,"tag":135,"props":313,"children":314},{"style":163},[315],{"type":28,"value":316},": {\n",{"type":23,"tag":135,"props":318,"children":320},{"class":137,"line":319},9,[321,326,331,336],{"type":23,"tag":135,"props":322,"children":323},{"style":173},[324],{"type":28,"value":325},"    \"identifiers\"",{"type":23,"tag":135,"props":327,"children":328},{"style":163},[329],{"type":28,"value":330},": [",{"type":23,"tag":135,"props":332,"children":333},{"style":184},[334],{"type":28,"value":335},"\"sph5048\"",{"type":23,"tag":135,"props":337,"children":338},{"style":163},[339],{"type":28,"value":340},"],\n",{"type":23,"tag":135,"props":342,"children":344},{"class":137,"line":343},10,[345,350,354],{"type":23,"tag":135,"props":346,"children":347},{"style":173},[348],{"type":28,"value":349},"    \"name\"",{"type":23,"tag":135,"props":351,"children":352},{"style":163},[353],{"type":28,"value":181},{"type":23,"tag":135,"props":355,"children":356},{"style":184},[357],{"type":28,"value":358},"\"Solar Inverter\"\n",{"type":23,"tag":135,"props":360,"children":362},{"class":137,"line":361},11,[363],{"type":23,"tag":135,"props":364,"children":365},{"style":163},[366],{"type":28,"value":367},"  }\n",{"type":23,"tag":135,"props":369,"children":371},{"class":137,"line":370},12,[372],{"type":23,"tag":135,"props":373,"children":374},{"style":163},[375],{"type":28,"value":376},"}\n",{"type":23,"tag":24,"props":378,"children":379},{},[380],{"type":28,"value":381},"My solar poller publishes about 30 of these at startup. Thirty sensors just appear, grouped under a \"Solar Inverter\" device, without me touching a single config file. That's the part that clicked for me. The overhead I'd always imagined wasn't there.",{"type":23,"tag":24,"props":383,"children":384},{},[385,387,393],{"type":28,"value":386},"MQTT as an integration layer also removes a problem I had with the old stack: everything had to speak HTTP and know about my custom API. Now anything that wants solar data subscribes to ",{"type":23,"tag":131,"props":388,"children":390},{"className":389},[],[391],{"type":28,"value":392},"solar/#",{"type":28,"value":394}," on the broker. Multiple consumers, one publisher, no coupling between them.",{"type":23,"tag":36,"props":396,"children":398},{"id":397},"the-data-looked-wrong",[399],{"type":28,"value":400},"The Data Looked Wrong",{"type":23,"tag":24,"props":402,"children":403},{},[404],{"type":28,"value":405},"A day or two after getting things running, I had some time to actually build out a solar dashboard. I wanted a view with daily production totals, some trend charts, and a quick read on current battery state. I pulled the data into Lovelace cards and the numbers immediately looked off.",{"type":23,"tag":24,"props":407,"children":408},{},[409],{"type":28,"value":410},"Some sensors were showing values that didn't match what I knew the inverter had been doing. Aggregates were misaligned. Daily totals weren't adding up in ways that made sense. The raw sensor readings were fine, it was the grouped and summarized data that was confusing.",{"type":23,"tag":24,"props":412,"children":413},{},[414],{"type":28,"value":415},"Two separate issues turned out to be responsible, both timezone-related, and both entirely my fault.",{"type":23,"tag":36,"props":417,"children":419},{"id":418},"timezone-issue-one-home-assistant",[420],{"type":28,"value":421},"Timezone Issue One: Home Assistant",{"type":23,"tag":24,"props":423,"children":424},{},[425],{"type":28,"value":426},"Home Assistant stores timestamps internally. If the timezone setting in Home Assistant doesn't match your actual timezone, those timestamps are wrong relative to your local time, and anything that depends on them, including Energy Dashboard bucketing and history graphs, shifts accordingly.",{"type":23,"tag":24,"props":428,"children":429},{},[430],{"type":28,"value":431},"The fix is in Settings > System > General. There's a timezone field. Mine wasn't set correctly. I updated it to the right timezone, restarted, and the history graphs snapped into alignment.",{"type":23,"tag":24,"props":433,"children":434},{},[435],{"type":28,"value":436},"This one is easy to overlook because the live sensor values look fine. The inverter is pushing a number, Home Assistant is displaying it, all is well. The timezone setting only reveals itself when you start asking \"what happened at 2pm yesterday\" and Home Assistant is working from a different definition of 2pm than you are.",{"type":23,"tag":24,"props":438,"children":439},{},[440,442,448],{"type":28,"value":441},"If you run Home Assistant in Docker, the container also needs the right timezone set. Passing ",{"type":23,"tag":131,"props":443,"children":445},{"className":444},[],[446],{"type":28,"value":447},"TZ",{"type":28,"value":449}," as an environment variable in your Compose file handles it:",{"type":23,"tag":125,"props":451,"children":455},{"className":452,"code":453,"language":454,"meta":8,"style":8},"language-yaml shiki shiki-themes github-dark","services:\n  homeassistant:\n    image: ghcr.io/home-assistant/home-assistant:stable\n    environment:\n      TZ: America/New_York\n","yaml",[456],{"type":23,"tag":131,"props":457,"children":458},{"__ignoreMap":8},[459,473,485,502,514],{"type":23,"tag":135,"props":460,"children":461},{"class":137,"line":138},[462,468],{"type":23,"tag":135,"props":463,"children":465},{"style":464},"--shiki-default:#85E89D",[466],{"type":28,"value":467},"services",{"type":23,"tag":135,"props":469,"children":470},{"style":163},[471],{"type":28,"value":472},":\n",{"type":23,"tag":135,"props":474,"children":475},{"class":137,"line":169},[476,481],{"type":23,"tag":135,"props":477,"children":478},{"style":464},[479],{"type":28,"value":480},"  homeassistant",{"type":23,"tag":135,"props":482,"children":483},{"style":163},[484],{"type":28,"value":472},{"type":23,"tag":135,"props":486,"children":487},{"class":137,"line":195},[488,493,497],{"type":23,"tag":135,"props":489,"children":490},{"style":464},[491],{"type":28,"value":492},"    image",{"type":23,"tag":135,"props":494,"children":495},{"style":163},[496],{"type":28,"value":181},{"type":23,"tag":135,"props":498,"children":499},{"style":184},[500],{"type":28,"value":501},"ghcr.io/home-assistant/home-assistant:stable\n",{"type":23,"tag":135,"props":503,"children":504},{"class":137,"line":217},[505,510],{"type":23,"tag":135,"props":506,"children":507},{"style":464},[508],{"type":28,"value":509},"    environment",{"type":23,"tag":135,"props":511,"children":512},{"style":163},[513],{"type":28,"value":472},{"type":23,"tag":135,"props":515,"children":516},{"class":137,"line":239},[517,522,526],{"type":23,"tag":135,"props":518,"children":519},{"style":464},[520],{"type":28,"value":521},"      TZ",{"type":23,"tag":135,"props":523,"children":524},{"style":163},[525],{"type":28,"value":181},{"type":23,"tag":135,"props":527,"children":528},{"style":184},[529],{"type":28,"value":530},"America/New_York\n",{"type":23,"tag":24,"props":532,"children":533},{},[534],{"type":28,"value":535},"The UI setting and the container environment variable are separate things. Both need to be correct.",{"type":23,"tag":36,"props":537,"children":539},{"id":538},"timezone-issue-two-timescaledb",[540],{"type":28,"value":541},"Timezone Issue Two: TimescaleDB",{"type":23,"tag":24,"props":543,"children":544},{},[545],{"type":28,"value":546},"The second issue was in my TimescaleDB setup. I use TimescaleDB for long-term storage of solar metrics, which I may write a full post on at some point because it's been a genuinely good tool for this use case. The short version is that it's PostgreSQL with a time-series extension that adds automatic data partitioning, compression, and continuous aggregates.",{"type":23,"tag":24,"props":548,"children":549},{},[550,552,563,565,571],{"type":28,"value":551},"The problem: TimescaleDB's ",{"type":23,"tag":48,"props":553,"children":556},{"href":554,"rel":555},"https://docs.timescale.com/api/latest/hyperfunctions/time_bucket/",[113],[557],{"type":23,"tag":131,"props":558,"children":560},{"className":559},[],[561],{"type":28,"value":562},"time_bucket",{"type":28,"value":564}," function, when used on ",{"type":23,"tag":131,"props":566,"children":568},{"className":567},[],[569],{"type":28,"value":570},"TIMESTAMPTZ",{"type":28,"value":572}," columns, buckets by UTC midnight by default. If you're in a timezone that's offset from UTC, your \"daily\" aggregates don't align to your actual days. A bucket labeled \"March 8\" might contain data from 7pm local time on March 7 through 6:59pm local time on March 8, depending on your offset.",{"type":23,"tag":24,"props":574,"children":575},{},[576],{"type":28,"value":577},"For a solar system, this matters a lot. Daily production totals are one of the most useful metrics, and if the day boundaries are shifted by several hours, the numbers look wrong and, more importantly, they are wrong.",{"type":23,"tag":125,"props":579,"children":583},{"className":580,"code":581,"language":582,"meta":8,"style":8},"language-sql shiki shiki-themes github-dark","-- This buckets by UTC midnight, which may not be what you want\nSELECT time_bucket('1 day', ts) AS bucket, sum(value)\nFROM inverter_metrics\nWHERE key = 'pv1_energy_kwh'\nGROUP BY bucket;\n","sql",[584],{"type":23,"tag":131,"props":585,"children":586},{"__ignoreMap":8},[587,595,603,611,619],{"type":23,"tag":135,"props":588,"children":589},{"class":137,"line":138},[590],{"type":23,"tag":135,"props":591,"children":592},{},[593],{"type":28,"value":594},"-- This buckets by UTC midnight, which may not be what you want\n",{"type":23,"tag":135,"props":596,"children":597},{"class":137,"line":169},[598],{"type":23,"tag":135,"props":599,"children":600},{},[601],{"type":28,"value":602},"SELECT time_bucket('1 day', ts) AS bucket, sum(value)\n",{"type":23,"tag":135,"props":604,"children":605},{"class":137,"line":195},[606],{"type":23,"tag":135,"props":607,"children":608},{},[609],{"type":28,"value":610},"FROM inverter_metrics\n",{"type":23,"tag":135,"props":612,"children":613},{"class":137,"line":217},[614],{"type":23,"tag":135,"props":615,"children":616},{},[617],{"type":28,"value":618},"WHERE key = 'pv1_energy_kwh'\n",{"type":23,"tag":135,"props":620,"children":621},{"class":137,"line":239},[622],{"type":23,"tag":135,"props":623,"children":624},{},[625],{"type":28,"value":626},"GROUP BY bucket;\n",{"type":23,"tag":24,"props":628,"children":629},{},[630,632,637,639,645,647,653,655,660],{"type":28,"value":631},"The fix has two parts. First, I set the ",{"type":23,"tag":131,"props":633,"children":635},{"className":634},[],[636],{"type":28,"value":447},{"type":28,"value":638}," environment variable in my Docker Compose ",{"type":23,"tag":131,"props":640,"children":642},{"className":641},[],[643],{"type":28,"value":644},".env",{"type":28,"value":646}," file and passed it through to the TimescaleDB container. PostgreSQL uses this for ",{"type":23,"tag":131,"props":648,"children":650},{"className":649},[],[651],{"type":28,"value":652},"CURRENT_TIMESTAMP",{"type":28,"value":654},", display formatting, and any operations that reference the session timezone — good baseline hygiene, but it doesn't actually move where ",{"type":23,"tag":131,"props":656,"children":658},{"className":657},[],[659],{"type":28,"value":562},{"type":28,"value":661}," draws its day boundaries.",{"type":23,"tag":125,"props":663,"children":665},{"className":452,"code":664,"language":454,"meta":8,"style":8},"# docker-compose.yml\nservices:\n  timescaledb:\n    image: timescale/timescaledb:latest-pg16\n    environment:\n      TZ: ${TZ}\n      POSTGRES_DB: solar\n      POSTGRES_USER: solar\n      POSTGRES_PASSWORD: ${DB_PASSWORD}\n",[666],{"type":23,"tag":131,"props":667,"children":668},{"__ignoreMap":8},[669,678,689,701,717,728,744,761,777],{"type":23,"tag":135,"props":670,"children":671},{"class":137,"line":138},[672],{"type":23,"tag":135,"props":673,"children":675},{"style":674},"--shiki-default:#6A737D",[676],{"type":28,"value":677},"# docker-compose.yml\n",{"type":23,"tag":135,"props":679,"children":680},{"class":137,"line":169},[681,685],{"type":23,"tag":135,"props":682,"children":683},{"style":464},[684],{"type":28,"value":467},{"type":23,"tag":135,"props":686,"children":687},{"style":163},[688],{"type":28,"value":472},{"type":23,"tag":135,"props":690,"children":691},{"class":137,"line":195},[692,697],{"type":23,"tag":135,"props":693,"children":694},{"style":464},[695],{"type":28,"value":696},"  timescaledb",{"type":23,"tag":135,"props":698,"children":699},{"style":163},[700],{"type":28,"value":472},{"type":23,"tag":135,"props":702,"children":703},{"class":137,"line":217},[704,708,712],{"type":23,"tag":135,"props":705,"children":706},{"style":464},[707],{"type":28,"value":492},{"type":23,"tag":135,"props":709,"children":710},{"style":163},[711],{"type":28,"value":181},{"type":23,"tag":135,"props":713,"children":714},{"style":184},[715],{"type":28,"value":716},"timescale/timescaledb:latest-pg16\n",{"type":23,"tag":135,"props":718,"children":719},{"class":137,"line":239},[720,724],{"type":23,"tag":135,"props":721,"children":722},{"style":464},[723],{"type":28,"value":509},{"type":23,"tag":135,"props":725,"children":726},{"style":163},[727],{"type":28,"value":472},{"type":23,"tag":135,"props":729,"children":730},{"class":137,"line":261},[731,735,739],{"type":23,"tag":135,"props":732,"children":733},{"style":464},[734],{"type":28,"value":521},{"type":23,"tag":135,"props":736,"children":737},{"style":163},[738],{"type":28,"value":181},{"type":23,"tag":135,"props":740,"children":741},{"style":184},[742],{"type":28,"value":743},"${TZ}\n",{"type":23,"tag":135,"props":745,"children":746},{"class":137,"line":283},[747,752,756],{"type":23,"tag":135,"props":748,"children":749},{"style":464},[750],{"type":28,"value":751},"      POSTGRES_DB",{"type":23,"tag":135,"props":753,"children":754},{"style":163},[755],{"type":28,"value":181},{"type":23,"tag":135,"props":757,"children":758},{"style":184},[759],{"type":28,"value":760},"solar\n",{"type":23,"tag":135,"props":762,"children":763},{"class":137,"line":305},[764,769,773],{"type":23,"tag":135,"props":765,"children":766},{"style":464},[767],{"type":28,"value":768},"      POSTGRES_USER",{"type":23,"tag":135,"props":770,"children":771},{"style":163},[772],{"type":28,"value":181},{"type":23,"tag":135,"props":774,"children":775},{"style":184},[776],{"type":28,"value":760},{"type":23,"tag":135,"props":778,"children":779},{"class":137,"line":319},[780,785,789],{"type":23,"tag":135,"props":781,"children":782},{"style":464},[783],{"type":28,"value":784},"      POSTGRES_PASSWORD",{"type":23,"tag":135,"props":786,"children":787},{"style":163},[788],{"type":28,"value":181},{"type":23,"tag":135,"props":790,"children":791},{"style":184},[792],{"type":28,"value":793},"${DB_PASSWORD}\n",{"type":23,"tag":125,"props":795,"children":799},{"className":796,"code":797,"language":798,"meta":8,"style":8},"language-bash shiki shiki-themes github-dark","# .env\nTZ=America/New_York\nDB_PASSWORD=...\n","bash",[800],{"type":23,"tag":131,"props":801,"children":802},{"__ignoreMap":8},[803,811,828],{"type":23,"tag":135,"props":804,"children":805},{"class":137,"line":138},[806],{"type":23,"tag":135,"props":807,"children":808},{"style":674},[809],{"type":28,"value":810},"# .env\n",{"type":23,"tag":135,"props":812,"children":813},{"class":137,"line":169},[814,818,824],{"type":23,"tag":135,"props":815,"children":816},{"style":163},[817],{"type":28,"value":447},{"type":23,"tag":135,"props":819,"children":821},{"style":820},"--shiki-default:#F97583",[822],{"type":28,"value":823},"=",{"type":23,"tag":135,"props":825,"children":826},{"style":184},[827],{"type":28,"value":530},{"type":23,"tag":135,"props":829,"children":830},{"class":137,"line":195},[831,836,840],{"type":23,"tag":135,"props":832,"children":833},{"style":163},[834],{"type":28,"value":835},"DB_PASSWORD",{"type":23,"tag":135,"props":837,"children":838},{"style":820},[839],{"type":28,"value":823},{"type":23,"tag":135,"props":841,"children":842},{"style":184},[843],{"type":28,"value":844},"...\n",{"type":23,"tag":24,"props":846,"children":847},{},[848,850,855,857,862,864,869],{"type":28,"value":849},"On ",{"type":23,"tag":131,"props":851,"children":853},{"className":852},[],[854],{"type":28,"value":570},{"type":28,"value":856}," columns, ",{"type":23,"tag":131,"props":858,"children":860},{"className":859},[],[861],{"type":28,"value":562},{"type":28,"value":863}," always buckets by UTC midnight regardless of the container's ",{"type":23,"tag":131,"props":865,"children":867},{"className":866},[],[868],{"type":28,"value":447},{"type":28,"value":870}," setting. The env var helps with display; the query itself needs fixing.",{"type":23,"tag":24,"props":872,"children":873},{},[874,876,881,883,889],{"type":28,"value":875},"For local-midnight bucketing, the ",{"type":23,"tag":131,"props":877,"children":879},{"className":878},[],[880],{"type":28,"value":562},{"type":28,"value":882}," function has an ",{"type":23,"tag":131,"props":884,"children":886},{"className":885},[],[887],{"type":28,"value":888},"origin",{"type":28,"value":890}," parameter that shifts the bucket start point:",{"type":23,"tag":125,"props":892,"children":894},{"className":580,"code":893,"language":582,"meta":8,"style":8},"-- Shift buckets to align with local midnight (UTC-5 example)\nSELECT time_bucket('1 day', ts, '2026-01-01 00:00:00-05'::timestamptz) AS bucket,\n       sum(value)\nFROM inverter_metrics\nWHERE key = 'pv1_energy_kwh'\nGROUP BY bucket;\n",[895],{"type":23,"tag":131,"props":896,"children":897},{"__ignoreMap":8},[898,906,914,922,929,936],{"type":23,"tag":135,"props":899,"children":900},{"class":137,"line":138},[901],{"type":23,"tag":135,"props":902,"children":903},{},[904],{"type":28,"value":905},"-- Shift buckets to align with local midnight (UTC-5 example)\n",{"type":23,"tag":135,"props":907,"children":908},{"class":137,"line":169},[909],{"type":23,"tag":135,"props":910,"children":911},{},[912],{"type":28,"value":913},"SELECT time_bucket('1 day', ts, '2026-01-01 00:00:00-05'::timestamptz) AS bucket,\n",{"type":23,"tag":135,"props":915,"children":916},{"class":137,"line":195},[917],{"type":23,"tag":135,"props":918,"children":919},{},[920],{"type":28,"value":921},"       sum(value)\n",{"type":23,"tag":135,"props":923,"children":924},{"class":137,"line":217},[925],{"type":23,"tag":135,"props":926,"children":927},{},[928],{"type":28,"value":610},{"type":23,"tag":135,"props":930,"children":931},{"class":137,"line":239},[932],{"type":23,"tag":135,"props":933,"children":934},{},[935],{"type":28,"value":618},{"type":23,"tag":135,"props":937,"children":938},{"class":137,"line":261},[939],{"type":23,"tag":135,"props":940,"children":941},{},[942],{"type":28,"value":626},{"type":23,"tag":24,"props":944,"children":945},{},[946],{"type":28,"value":947},"Or you can cast the timestamp to a specific timezone before bucketing:",{"type":23,"tag":125,"props":949,"children":951},{"className":580,"code":950,"language":582,"meta":8,"style":8},"SELECT time_bucket('1 day', ts AT TIME ZONE 'America/New_York') AS local_bucket,\n       sum(value)\nFROM inverter_metrics\nWHERE key = 'pv1_energy_kwh'\nGROUP BY local_bucket;\n",[952],{"type":23,"tag":131,"props":953,"children":954},{"__ignoreMap":8},[955,963,970,977,984],{"type":23,"tag":135,"props":956,"children":957},{"class":137,"line":138},[958],{"type":23,"tag":135,"props":959,"children":960},{},[961],{"type":28,"value":962},"SELECT time_bucket('1 day', ts AT TIME ZONE 'America/New_York') AS local_bucket,\n",{"type":23,"tag":135,"props":964,"children":965},{"class":137,"line":169},[966],{"type":23,"tag":135,"props":967,"children":968},{},[969],{"type":28,"value":921},{"type":23,"tag":135,"props":971,"children":972},{"class":137,"line":195},[973],{"type":23,"tag":135,"props":974,"children":975},{},[976],{"type":28,"value":610},{"type":23,"tag":135,"props":978,"children":979},{"class":137,"line":217},[980],{"type":23,"tag":135,"props":981,"children":982},{},[983],{"type":28,"value":618},{"type":23,"tag":135,"props":985,"children":986},{"class":137,"line":239},[987],{"type":23,"tag":135,"props":988,"children":989},{},[990],{"type":28,"value":991},"GROUP BY local_bucket;\n",{"type":23,"tag":24,"props":993,"children":994},{},[995,997,1003],{"type":28,"value":996},"The ",{"type":23,"tag":131,"props":998,"children":1000},{"className":999},[],[1001],{"type":28,"value":1002},"AT TIME ZONE",{"type":28,"value":1004}," approach handles daylight saving automatically and is easier to read. The data already stored with UTC-offset timestamps won't retroactively fix itself, so the aggregates for the previous few days will still look a bit off. The new data should start bucketing correctly right away.",{"type":23,"tag":36,"props":1006,"children":1008},{"id":1007},"what-this-weekend-produced",[1009],{"type":28,"value":1010},"What This Weekend Produced",{"type":23,"tag":24,"props":1012,"children":1013},{},[1014],{"type":28,"value":1015},"A day to get Home Assistant running and data flowing. Another day to sort out the dashboards and fix both timezone issues. Net result: a solar monitoring setup that's actually better than what I had before, maintained largely by tools I didn't write, and running reliably since I got it working.",{"type":23,"tag":24,"props":1017,"children":1018},{},[1019],{"type":28,"value":1020},"MQTT clicked faster than expected, and the timezone issues were the kind of setup learning curve you only run into once. Both came down to setting an environment variable in the right place. Neither required code changes or schema migrations.",{"type":23,"tag":24,"props":1022,"children":1023},{},[1024],{"type":28,"value":1025},"TimescaleDB is still earning its spot in the stack. I'll probably write more about it separately, the continuous aggregates feature alone warrants its own writeup.",{"type":23,"tag":1027,"props":1028,"children":1029},"style",{},[1030],{"type":28,"value":1031},"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":8,"searchDepth":169,"depth":169,"links":1033},[1034,1035,1036,1037,1038,1039,1040],{"id":38,"depth":169,"text":41},{"id":79,"depth":169,"text":82},{"id":95,"depth":169,"text":98},{"id":397,"depth":169,"text":400},{"id":418,"depth":169,"text":421},{"id":538,"depth":169,"text":541},{"id":1007,"depth":169,"text":1010},"markdown","content:blog:home-assistant-mqtt-and-the-timezone-confusion-i-made-myself.md","content","blog/home-assistant-mqtt-and-the-timezone-confusion-i-made-myself.md","blog/home-assistant-mqtt-and-the-timezone-confusion-i-made-myself","md",{"_path":63,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":1048,"description":1049,"date":11,"updated":11,"tags":1050,"series":1053,"seriesPart":138,"readingTime":1054,"cover":8,"body":1055,"_type":1041,"_id":1799,"_source":1043,"_file":1800,"_stem":1801,"_extension":1046},"DIY Solar Monitoring With a Sungoldpower SPH5048: The Python Build","How I built a custom solar inverter monitoring stack with Python, Modbus RTU, TimescaleDB, and a hand-rolled dashboard — and how a PSU cable killed it.",[16,15,1051,1052,17],"python","modbus","Solar Monitoring Stack","8 min read",{"type":20,"children":1056,"toc":1788},[1057,1069,1073,1078,1083,1097,1102,1107,1113,1126,1131,1136,1171,1177,1182,1187,1192,1205,1371,1376,1383,1388,1407,1417,1544,1554,1560,1565,1642,1655,1660,1666,1671,1676,1681,1686,1692,1700,1705,1711,1716,1721,1726,1732,1737,1749,1754,1759,1764,1767,1772,1784],{"type":23,"tag":24,"props":1058,"children":1059},{},[1060,1062,1067],{"type":28,"value":1061},"This is part one of a two-part series on building a DIY solar inverter monitoring system for a Sungoldpower SPH5048. ",{"type":23,"tag":48,"props":1063,"children":1064},{"href":71},[1065],{"type":28,"value":1066},"Part two covers the rebuild",{"type":28,"value":1068}," after the whole stack was taken out by a PSU cable mistake.",{"type":23,"tag":1070,"props":1071,"children":1072},"hr",{},[],{"type":23,"tag":24,"props":1074,"children":1075},{},[1076],{"type":28,"value":1077},"When I first got my solar setup running, I did what any homelab person does: I wanted numbers. Not just the numbers on the inverter's tiny LCD, but historical numbers, trending numbers, \"why did the battery only charge to 80% yesterday\" numbers.",{"type":23,"tag":24,"props":1079,"children":1080},{},[1081],{"type":28,"value":1082},"The Sungoldpower SPH5048 is a 5 kW, 48V hybrid/off-grid inverter with split-phase output. It does a lot of things well. Out-of-the-box monitoring is not one of them. The bundled app is unreliable on a local network, and I had no interest in sending my power data to a cloud I don't control.",{"type":23,"tag":24,"props":1084,"children":1085},{},[1086,1088,1095],{"type":28,"value":1087},"My first attempt at fixing this was the easy path: I paid for ",{"type":23,"tag":48,"props":1089,"children":1092},{"href":1090,"rel":1091},"https://solar-assistant.io",[113],[1093],{"type":28,"value":1094},"Solar Assistant",{"type":28,"value":1096}," and ran it on a Raspberry Pi. It was a solid solution for about three months, until the Pi's SD card corrupted and took everything with it. That was the first time I lost all my solar tracking data, and I had nothing running for the next four months while I figured out what to do next.",{"type":23,"tag":24,"props":1098,"children":1099},{},[1100],{"type":28,"value":1101},"The answer I landed on was to build my own stack. That way I understood every piece, could fix every piece, and wasn't dependent on anyone else's hardware or subscription.",{"type":23,"tag":24,"props":1103,"children":1104},{},[1105],{"type":28,"value":1106},"So I built my own stack. And it worked great, right up until it didn't.",{"type":23,"tag":36,"props":1108,"children":1110},{"id":1109},"the-hardware-connection",[1111],{"type":28,"value":1112},"The Hardware Connection",{"type":23,"tag":24,"props":1114,"children":1115},{},[1116,1118,1124],{"type":28,"value":1117},"The SPH5048 exposes a Modbus RTU interface over a USB-RS485 adapter. You plug the adapter into the inverter's RS485 port, plug the other end into a USB port on your server, and the inverter shows up as ",{"type":23,"tag":131,"props":1119,"children":1121},{"className":1120},[],[1122],{"type":28,"value":1123},"/dev/ttyUSB0",{"type":28,"value":1125}," (or similar, depending on your udev rules).",{"type":23,"tag":24,"props":1127,"children":1128},{},[1129],{"type":28,"value":1130},"Modbus RTU is a serial protocol. It's old, reliable, and almost entirely undocumented for any specific device unless you happen to have the right PDF. I eventually found a register map for a close enough variant of the SPH5048 and started mapping registers to the metrics I cared about.",{"type":23,"tag":24,"props":1132,"children":1133},{},[1134],{"type":28,"value":1135},"What I wanted to track:",{"type":23,"tag":1137,"props":1138,"children":1139},"ul",{},[1140,1146,1151,1156,1161,1166],{"type":23,"tag":1141,"props":1142,"children":1143},"li",{},[1144],{"type":28,"value":1145},"Battery state of charge (SOC), voltage, current",{"type":23,"tag":1141,"props":1147,"children":1148},{},[1149],{"type":28,"value":1150},"PV1 power, voltage, current (I have one PV string)",{"type":23,"tag":1141,"props":1152,"children":1153},{},[1154],{"type":28,"value":1155},"Grid power, voltage, frequency",{"type":23,"tag":1141,"props":1157,"children":1158},{},[1159],{"type":28,"value":1160},"Load power and apparent power",{"type":23,"tag":1141,"props":1162,"children":1163},{},[1164],{"type":28,"value":1165},"Inverter temperatures",{"type":23,"tag":1141,"props":1167,"children":1168},{},[1169],{"type":28,"value":1170},"Inverter state (charging, discharging, grid-tied, island mode)",{"type":23,"tag":36,"props":1172,"children":1174},{"id":1173},"the-poller",[1175],{"type":28,"value":1176},"The Poller",{"type":23,"tag":24,"props":1178,"children":1179},{},[1180],{"type":28,"value":1181},"Before I had a working poller, I had a very much not-working one.",{"type":23,"tag":24,"props":1183,"children":1184},{},[1185],{"type":28,"value":1186},"My first instinct was to probe the serial connection to figure out what the inverter was doing. Not structured Modbus reads, just throwing bytes at the port to see what came back. What came back was a voltage anomaly. I had flooded the USB/serial interface badly enough that the inverter briefly took everything connected to it offline. No damage, but it was a genuinely scary few seconds watching my whole solar setup drop.",{"type":23,"tag":24,"props":1188,"children":1189},{},[1190],{"type":28,"value":1191},"After consulting with an AI about what I was doing wrong, I learned I had been going about it completely backwards. The inverter expects structured Modbus RTU requests. Not probing, not discovery, just: send the right frame, read the response, move on. Once I understood that, everything got simpler.",{"type":23,"tag":24,"props":1193,"children":1194},{},[1195,1197,1203],{"type":28,"value":1196},"The Python script that read all of this was built around ",{"type":23,"tag":131,"props":1198,"children":1200},{"className":1199},[],[1201],{"type":28,"value":1202},"pymodbus",{"type":28,"value":1204},". A polling loop ran every 5 seconds, reading register blocks from the inverter and writing the results to TimescaleDB.",{"type":23,"tag":125,"props":1206,"children":1209},{"code":1207,"language":1051,"meta":8,"className":1208,"style":8},"from pymodbus.client import ModbusSerialClient\n\nclient = ModbusSerialClient(\n    port=\"/dev/ttyUSB0\",\n    baudrate=9600,\n    parity=\"N\",\n    stopbits=1,\n    bytesize=8,\n    timeout=3,\n)\n\ndef read_register(address, count=1, unit=1):\n    result = client.read_input_registers(address, count, slave=unit)\n    if result.isError():\n        # Some registers are holding registers, not input registers\n        result = client.read_holding_registers(address, count, slave=unit)\n    if result.isError():\n        return None\n    return result.registers\n","language-python shiki shiki-themes github-dark",[1210],{"type":23,"tag":131,"props":1211,"children":1212},{"__ignoreMap":8},[1213,1221,1230,1238,1246,1254,1262,1270,1278,1286,1294,1301,1309,1318,1327,1336,1345,1353,1362],{"type":23,"tag":135,"props":1214,"children":1215},{"class":137,"line":138},[1216],{"type":23,"tag":135,"props":1217,"children":1218},{},[1219],{"type":28,"value":1220},"from pymodbus.client import ModbusSerialClient\n",{"type":23,"tag":135,"props":1222,"children":1223},{"class":137,"line":169},[1224],{"type":23,"tag":135,"props":1225,"children":1227},{"emptyLinePlaceholder":1226},true,[1228],{"type":28,"value":1229},"\n",{"type":23,"tag":135,"props":1231,"children":1232},{"class":137,"line":195},[1233],{"type":23,"tag":135,"props":1234,"children":1235},{},[1236],{"type":28,"value":1237},"client = ModbusSerialClient(\n",{"type":23,"tag":135,"props":1239,"children":1240},{"class":137,"line":217},[1241],{"type":23,"tag":135,"props":1242,"children":1243},{},[1244],{"type":28,"value":1245},"    port=\"/dev/ttyUSB0\",\n",{"type":23,"tag":135,"props":1247,"children":1248},{"class":137,"line":239},[1249],{"type":23,"tag":135,"props":1250,"children":1251},{},[1252],{"type":28,"value":1253},"    baudrate=9600,\n",{"type":23,"tag":135,"props":1255,"children":1256},{"class":137,"line":261},[1257],{"type":23,"tag":135,"props":1258,"children":1259},{},[1260],{"type":28,"value":1261},"    parity=\"N\",\n",{"type":23,"tag":135,"props":1263,"children":1264},{"class":137,"line":283},[1265],{"type":23,"tag":135,"props":1266,"children":1267},{},[1268],{"type":28,"value":1269},"    stopbits=1,\n",{"type":23,"tag":135,"props":1271,"children":1272},{"class":137,"line":305},[1273],{"type":23,"tag":135,"props":1274,"children":1275},{},[1276],{"type":28,"value":1277},"    bytesize=8,\n",{"type":23,"tag":135,"props":1279,"children":1280},{"class":137,"line":319},[1281],{"type":23,"tag":135,"props":1282,"children":1283},{},[1284],{"type":28,"value":1285},"    timeout=3,\n",{"type":23,"tag":135,"props":1287,"children":1288},{"class":137,"line":343},[1289],{"type":23,"tag":135,"props":1290,"children":1291},{},[1292],{"type":28,"value":1293},")\n",{"type":23,"tag":135,"props":1295,"children":1296},{"class":137,"line":361},[1297],{"type":23,"tag":135,"props":1298,"children":1299},{"emptyLinePlaceholder":1226},[1300],{"type":28,"value":1229},{"type":23,"tag":135,"props":1302,"children":1303},{"class":137,"line":370},[1304],{"type":23,"tag":135,"props":1305,"children":1306},{},[1307],{"type":28,"value":1308},"def read_register(address, count=1, unit=1):\n",{"type":23,"tag":135,"props":1310,"children":1312},{"class":137,"line":1311},13,[1313],{"type":23,"tag":135,"props":1314,"children":1315},{},[1316],{"type":28,"value":1317},"    result = client.read_input_registers(address, count, slave=unit)\n",{"type":23,"tag":135,"props":1319,"children":1321},{"class":137,"line":1320},14,[1322],{"type":23,"tag":135,"props":1323,"children":1324},{},[1325],{"type":28,"value":1326},"    if result.isError():\n",{"type":23,"tag":135,"props":1328,"children":1330},{"class":137,"line":1329},15,[1331],{"type":23,"tag":135,"props":1332,"children":1333},{},[1334],{"type":28,"value":1335},"        # Some registers are holding registers, not input registers\n",{"type":23,"tag":135,"props":1337,"children":1339},{"class":137,"line":1338},16,[1340],{"type":23,"tag":135,"props":1341,"children":1342},{},[1343],{"type":28,"value":1344},"        result = client.read_holding_registers(address, count, slave=unit)\n",{"type":23,"tag":135,"props":1346,"children":1348},{"class":137,"line":1347},17,[1349],{"type":23,"tag":135,"props":1350,"children":1351},{},[1352],{"type":28,"value":1326},{"type":23,"tag":135,"props":1354,"children":1356},{"class":137,"line":1355},18,[1357],{"type":23,"tag":135,"props":1358,"children":1359},{},[1360],{"type":28,"value":1361},"        return None\n",{"type":23,"tag":135,"props":1363,"children":1365},{"class":137,"line":1364},19,[1366],{"type":23,"tag":135,"props":1367,"children":1368},{},[1369],{"type":28,"value":1370},"    return result.registers\n",{"type":23,"tag":24,"props":1372,"children":1373},{},[1374],{"type":28,"value":1375},"That fallback between input registers (function code 0x04) and holding registers (function code 0x03) turned out to be load-bearing. The SPH5048 uses both, and the register map doesn't always tell you which is which. You have to try one and fall back to the other.",{"type":23,"tag":1377,"props":1378,"children":1380},"h3",{"id":1379},"the-modbus-quirks-nobody-warned-me-about",[1381],{"type":28,"value":1382},"The Modbus Quirks Nobody Warned Me About",{"type":23,"tag":24,"props":1384,"children":1385},{},[1386],{"type":28,"value":1387},"Modbus RTU sounds simple on paper. In practice, it has opinions.",{"type":23,"tag":24,"props":1389,"children":1390},{},[1391,1397,1399,1405],{"type":23,"tag":1392,"props":1393,"children":1394},"strong",{},[1395],{"type":28,"value":1396},"Not every documented register is implemented.",{"type":28,"value":1398}," The register map listed PV2 registers and a set of P02 area addresses. When I queried them, the inverter returned ",{"type":23,"tag":131,"props":1400,"children":1402},{"className":1401},[],[1403],{"type":28,"value":1404},"Illegal data address",{"type":28,"value":1406}," errors. They just don't exist on this hardware revision, even though they're in the spec. I had to build a hardcoded allowlist of registers that actually work.",{"type":23,"tag":24,"props":1408,"children":1409},{},[1410,1415],{"type":23,"tag":1392,"props":1411,"children":1412},{},[1413],{"type":28,"value":1414},"Reading too many registers in one request fails.",{"type":28,"value":1416}," Modbus has a limit on how many registers you can read in a single request, but the real constraint here was more subtle: the inverter rejects requests that span non-contiguous register areas, even if the span is within spec limits. The solution was to group only contiguous registers into minimal read blocks and issue one request per block.",{"type":23,"tag":125,"props":1418,"children":1420},{"code":1419,"language":1051,"meta":8,"className":1208,"style":8},"def group_contiguous(registers):\n    \"\"\"Group registers into contiguous blocks for efficient batch reads.\"\"\"\n    if not registers:\n        return []\n    sorted_regs = sorted(registers)\n    blocks = []\n    start = sorted_regs[0]\n    prev = sorted_regs[0]\n    for reg in sorted_regs[1:]:\n        if reg != prev + 1:\n            blocks.append((start, prev - start + 1))\n            start = reg\n        prev = reg\n    blocks.append((start, prev - start + 1))\n    return blocks\n",[1421],{"type":23,"tag":131,"props":1422,"children":1423},{"__ignoreMap":8},[1424,1432,1440,1448,1456,1464,1472,1480,1488,1496,1504,1512,1520,1528,1536],{"type":23,"tag":135,"props":1425,"children":1426},{"class":137,"line":138},[1427],{"type":23,"tag":135,"props":1428,"children":1429},{},[1430],{"type":28,"value":1431},"def group_contiguous(registers):\n",{"type":23,"tag":135,"props":1433,"children":1434},{"class":137,"line":169},[1435],{"type":23,"tag":135,"props":1436,"children":1437},{},[1438],{"type":28,"value":1439},"    \"\"\"Group registers into contiguous blocks for efficient batch reads.\"\"\"\n",{"type":23,"tag":135,"props":1441,"children":1442},{"class":137,"line":195},[1443],{"type":23,"tag":135,"props":1444,"children":1445},{},[1446],{"type":28,"value":1447},"    if not registers:\n",{"type":23,"tag":135,"props":1449,"children":1450},{"class":137,"line":217},[1451],{"type":23,"tag":135,"props":1452,"children":1453},{},[1454],{"type":28,"value":1455},"        return []\n",{"type":23,"tag":135,"props":1457,"children":1458},{"class":137,"line":239},[1459],{"type":23,"tag":135,"props":1460,"children":1461},{},[1462],{"type":28,"value":1463},"    sorted_regs = sorted(registers)\n",{"type":23,"tag":135,"props":1465,"children":1466},{"class":137,"line":261},[1467],{"type":23,"tag":135,"props":1468,"children":1469},{},[1470],{"type":28,"value":1471},"    blocks = []\n",{"type":23,"tag":135,"props":1473,"children":1474},{"class":137,"line":283},[1475],{"type":23,"tag":135,"props":1476,"children":1477},{},[1478],{"type":28,"value":1479},"    start = sorted_regs[0]\n",{"type":23,"tag":135,"props":1481,"children":1482},{"class":137,"line":305},[1483],{"type":23,"tag":135,"props":1484,"children":1485},{},[1486],{"type":28,"value":1487},"    prev = sorted_regs[0]\n",{"type":23,"tag":135,"props":1489,"children":1490},{"class":137,"line":319},[1491],{"type":23,"tag":135,"props":1492,"children":1493},{},[1494],{"type":28,"value":1495},"    for reg in sorted_regs[1:]:\n",{"type":23,"tag":135,"props":1497,"children":1498},{"class":137,"line":343},[1499],{"type":23,"tag":135,"props":1500,"children":1501},{},[1502],{"type":28,"value":1503},"        if reg != prev + 1:\n",{"type":23,"tag":135,"props":1505,"children":1506},{"class":137,"line":361},[1507],{"type":23,"tag":135,"props":1508,"children":1509},{},[1510],{"type":28,"value":1511},"            blocks.append((start, prev - start + 1))\n",{"type":23,"tag":135,"props":1513,"children":1514},{"class":137,"line":370},[1515],{"type":23,"tag":135,"props":1516,"children":1517},{},[1518],{"type":28,"value":1519},"            start = reg\n",{"type":23,"tag":135,"props":1521,"children":1522},{"class":137,"line":1311},[1523],{"type":23,"tag":135,"props":1524,"children":1525},{},[1526],{"type":28,"value":1527},"        prev = reg\n",{"type":23,"tag":135,"props":1529,"children":1530},{"class":137,"line":1320},[1531],{"type":23,"tag":135,"props":1532,"children":1533},{},[1534],{"type":28,"value":1535},"    blocks.append((start, prev - start + 1))\n",{"type":23,"tag":135,"props":1537,"children":1538},{"class":137,"line":1329},[1539],{"type":23,"tag":135,"props":1540,"children":1541},{},[1542],{"type":28,"value":1543},"    return blocks\n",{"type":23,"tag":24,"props":1545,"children":1546},{},[1547,1552],{"type":23,"tag":1392,"props":1548,"children":1549},{},[1550],{"type":28,"value":1551},"The baud rate matters more than you'd think.",{"type":28,"value":1553}," At 9600 baud, a 10-second polling interval is fine. Push it faster and you start seeing timeout errors, especially if anything else on the serial bus is competing. I left it at 5 seconds and never had issues.",{"type":23,"tag":36,"props":1555,"children":1557},{"id":1556},"the-database-timescaledb",[1558],{"type":28,"value":1559},"The Database: TimescaleDB",{"type":23,"tag":24,"props":1561,"children":1562},{},[1563],{"type":28,"value":1564},"I used TimescaleDB for storage, which is a PostgreSQL extension that turns a regular table into a time-series hypertable. The schema was intentionally simple:",{"type":23,"tag":125,"props":1566,"children":1568},{"code":1567,"language":582,"meta":8,"className":580,"style":8},"CREATE TABLE inverter_metrics (\n    ts        TIMESTAMPTZ NOT NULL,\n    key       TEXT        NOT NULL,\n    value     DOUBLE PRECISION\n);\n\nSELECT create_hypertable('inverter_metrics', 'ts');\n\nCREATE INDEX ON inverter_metrics (key, ts DESC);\n",[1569],{"type":23,"tag":131,"props":1570,"children":1571},{"__ignoreMap":8},[1572,1580,1588,1596,1604,1612,1619,1627,1634],{"type":23,"tag":135,"props":1573,"children":1574},{"class":137,"line":138},[1575],{"type":23,"tag":135,"props":1576,"children":1577},{},[1578],{"type":28,"value":1579},"CREATE TABLE inverter_metrics (\n",{"type":23,"tag":135,"props":1581,"children":1582},{"class":137,"line":169},[1583],{"type":23,"tag":135,"props":1584,"children":1585},{},[1586],{"type":28,"value":1587},"    ts        TIMESTAMPTZ NOT NULL,\n",{"type":23,"tag":135,"props":1589,"children":1590},{"class":137,"line":195},[1591],{"type":23,"tag":135,"props":1592,"children":1593},{},[1594],{"type":28,"value":1595},"    key       TEXT        NOT NULL,\n",{"type":23,"tag":135,"props":1597,"children":1598},{"class":137,"line":217},[1599],{"type":23,"tag":135,"props":1600,"children":1601},{},[1602],{"type":28,"value":1603},"    value     DOUBLE PRECISION\n",{"type":23,"tag":135,"props":1605,"children":1606},{"class":137,"line":239},[1607],{"type":23,"tag":135,"props":1608,"children":1609},{},[1610],{"type":28,"value":1611},");\n",{"type":23,"tag":135,"props":1613,"children":1614},{"class":137,"line":261},[1615],{"type":23,"tag":135,"props":1616,"children":1617},{"emptyLinePlaceholder":1226},[1618],{"type":28,"value":1229},{"type":23,"tag":135,"props":1620,"children":1621},{"class":137,"line":283},[1622],{"type":23,"tag":135,"props":1623,"children":1624},{},[1625],{"type":28,"value":1626},"SELECT create_hypertable('inverter_metrics', 'ts');\n",{"type":23,"tag":135,"props":1628,"children":1629},{"class":137,"line":305},[1630],{"type":23,"tag":135,"props":1631,"children":1632},{"emptyLinePlaceholder":1226},[1633],{"type":28,"value":1229},{"type":23,"tag":135,"props":1635,"children":1636},{"class":137,"line":319},[1637],{"type":23,"tag":135,"props":1638,"children":1639},{},[1640],{"type":28,"value":1641},"CREATE INDEX ON inverter_metrics (key, ts DESC);\n",{"type":23,"tag":24,"props":1643,"children":1644},{},[1645,1647,1653],{"type":28,"value":1646},"Every metric stored as a ",{"type":23,"tag":131,"props":1648,"children":1650},{"className":1649},[],[1651],{"type":28,"value":1652},"(timestamp, key, value)",{"type":28,"value":1654}," row. Not the most query-friendly schema for wide tables, but simple to write to and easy to add new metrics without migrations. The inverter had about 30 metrics, so the data volume was manageable: roughly 360 rows every 30 seconds, about 43,000 rows per hour.",{"type":23,"tag":24,"props":1656,"children":1657},{},[1658],{"type":28,"value":1659},"TimescaleDB's automatic chunk management and compression made this basically zero-maintenance. I turned on chunk compression after 7 days and it kept the database small without any application-level work.",{"type":23,"tag":36,"props":1661,"children":1663},{"id":1662},"the-api-and-dashboard",[1664],{"type":28,"value":1665},"The API and Dashboard",{"type":23,"tag":24,"props":1667,"children":1668},{},[1669],{"type":28,"value":1670},"Rather than expose TimescaleDB directly to the browser, I put a lightweight TypeScript REST API in front of it. It handled current readings, historical data over a window, and daily/hourly aggregates. Nothing fancy, just a thin layer between the database and the browser.",{"type":23,"tag":24,"props":1672,"children":1673},{},[1674],{"type":28,"value":1675},"The dashboard was hand-built HTML, CSS, and vanilla JavaScript. Power flow diagram in the center, SOC gauge on the left, historical charts at the bottom using Chart.js. It looked exactly like what it was: a personal tool someone built over a few weekends.",{"type":23,"tag":24,"props":1677,"children":1678},{},[1679],{"type":28,"value":1680},"Building it was genuinely enjoyable. Watching live SOC numbers tick up as the sun came over the panels, seeing the power flow diagram animate between charging and discharging — that was satisfying in a way that a commercial monitoring app would never be, because I made all of it.",{"type":23,"tag":24,"props":1682,"children":1683},{},[1684],{"type":28,"value":1685},"Maintaining it was less enjoyable. Every time I wanted to add a metric or change a chart, I was wading back into three layers of code: the schema, the API endpoint, and the dashboard component. Nothing was hard individually. Together, it added up.",{"type":23,"tag":36,"props":1687,"children":1689},{"id":1688},"the-stack-visualized",[1690],{"type":28,"value":1691},"The Stack, Visualized",{"type":23,"tag":125,"props":1693,"children":1695},{"code":1694},"Sungoldpower SPH5048\n        |\n   RS485 / USB\n        |\n  Python Poller (pymodbus)\n  [10-second poll]\n        |\n  TimescaleDB (PostgreSQL)\n        |\n  TypeScript REST API\n        |\n  Custom HTML/JS Dashboard\n",[1696],{"type":23,"tag":131,"props":1697,"children":1698},{"__ignoreMap":8},[1699],{"type":28,"value":1694},{"type":23,"tag":24,"props":1701,"children":1702},{},[1703],{"type":28,"value":1704},"It was simple at the architecture level. Everything was custom at the implementation level.",{"type":23,"tag":36,"props":1706,"children":1708},{"id":1707},"what-worked",[1709],{"type":28,"value":1710},"What Worked",{"type":23,"tag":24,"props":1712,"children":1713},{},[1714],{"type":28,"value":1715},"The setup ran well. The poller reconnected automatically if the serial connection dropped. TimescaleDB never complained. I had about 10-11 days of 10-second resolution solar data and could query any of it.",{"type":23,"tag":24,"props":1717,"children":1718},{},[1719],{"type":28,"value":1720},"I could see exactly when the battery hit 100% and the inverter shifted to grid export. I could track the effect of season changes on daily solar yield. I could see the load jump when the HVAC turned on.",{"type":23,"tag":24,"props":1722,"children":1723},{},[1724],{"type":28,"value":1725},"The thing I wanted from the beginning, continuous visibility into my own power system, was working.",{"type":23,"tag":36,"props":1727,"children":1729},{"id":1728},"the-loss",[1730],{"type":28,"value":1731},"The Loss",{"type":23,"tag":24,"props":1733,"children":1734},{},[1735],{"type":28,"value":1736},"Then I killed it myself.",{"type":23,"tag":24,"props":1738,"children":1739},{},[1740,1742,1747],{"type":28,"value":1741},"While doing a server upgrade, I mixed a Corsair SATA power cable with an EVGA PSU. The physical connector fits. The pinouts do not match. I wrote an entire post about that incident ",{"type":23,"tag":48,"props":1743,"children":1744},{"href":50},[1745],{"type":28,"value":1746},"here",{"type":28,"value":1748},", but the short version is: the mismatch sent wrong voltages to the drives and that was that.",{"type":23,"tag":24,"props":1750,"children":1751},{},[1752],{"type":28,"value":1753},"The server came back up. The custom code did not. I had pushed exactly nothing to a repository. The Python poller, the TypeScript API, the dashboard templates, the Chart.js configuration — all of it was gone. I could recreate the TimescaleDB schema from memory, but the application code existed only on that machine.",{"type":23,"tag":24,"props":1755,"children":1756},{},[1757],{"type":28,"value":1758},"Second time losing solar monitoring data. This one stung more because I built it myself.",{"type":23,"tag":24,"props":1760,"children":1761},{},[1762],{"type":28,"value":1763},"If you have custom code sitting only on a local machine, please go push it somewhere right now. I'll wait.",{"type":23,"tag":1070,"props":1765,"children":1766},{},[],{"type":23,"tag":24,"props":1768,"children":1769},{},[1770],{"type":28,"value":1771},"The good news is it forced a rethink. The rebuild wasn't just a restore operation. It was a chance to reconsider what the original stack was actually costing me to run and make different choices.",{"type":23,"tag":24,"props":1773,"children":1774},{},[1775,1777,1782],{"type":28,"value":1776},"That's what ",{"type":23,"tag":48,"props":1778,"children":1779},{"href":71},[1780],{"type":28,"value":1781},"part two is about",{"type":28,"value":1783},".",{"type":23,"tag":1027,"props":1785,"children":1786},{},[1787],{"type":28,"value":1031},{"title":8,"searchDepth":169,"depth":169,"links":1789},[1790,1791,1794,1795,1796,1797,1798],{"id":1109,"depth":169,"text":1112},{"id":1173,"depth":169,"text":1176,"children":1792},[1793],{"id":1379,"depth":195,"text":1382},{"id":1556,"depth":169,"text":1559},{"id":1662,"depth":169,"text":1665},{"id":1688,"depth":169,"text":1691},{"id":1707,"depth":169,"text":1710},{"id":1728,"depth":169,"text":1731},"content:blog:solar-monitoring-part-1-the-python-build.md","blog/solar-monitoring-part-1-the-python-build.md","blog/solar-monitoring-part-1-the-python-build",{"_path":71,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":1803,"description":1804,"date":11,"updated":11,"tags":1805,"series":1053,"seriesPart":169,"readingTime":1807,"cover":8,"body":1808,"_type":1041,"_id":5333,"_source":1043,"_file":5334,"_stem":5335,"_extension":1046},"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.",[16,15,1806,14,13,17],"typescript","9 min read",{"type":20,"children":1809,"toc":5320},[1810,1822,1825,1830,1842,1855,1861,1866,1871,1877,1897,2158,2163,2742,2747,2753,2758,3351,3356,3362,3367,3372,3380,3385,3391,3396,3401,3848,3853,3866,3872,3885,3890,4520,4525,4537,4543,4555,4560,4692,4705,4710,4716,4721,5192,5204,5216,5222,5232,5249,5267,5277,5283,5288,5293,5298,5303,5306,5311,5316],{"type":23,"tag":24,"props":1811,"children":1812},{},[1813,1815,1820],{"type":28,"value":1814},"This is part two of a two-part series. ",{"type":23,"tag":48,"props":1816,"children":1817},{"href":63},[1818],{"type":28,"value":1819},"Part one covers the original build",{"type":28,"value":1821}," and how it ended. This is the story of what came next.",{"type":23,"tag":1070,"props":1823,"children":1824},{},[],{"type":23,"tag":24,"props":1826,"children":1827},{},[1828],{"type":28,"value":1829},"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":23,"tag":24,"props":1831,"children":1832},{},[1833,1835,1840],{"type":28,"value":1834},"After a ",{"type":23,"tag":48,"props":1836,"children":1837},{"href":50},[1838],{"type":28,"value":1839},"PSU cable mix-up",{"type":28,"value":1841}," 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":23,"tag":24,"props":1843,"children":1844},{},[1845,1847,1853],{"type":28,"value":1846},"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":23,"tag":131,"props":1848,"children":1850},{"className":1849},[],[1851],{"type":28,"value":1852},"git push",{"type":28,"value":1854},", and adding a new sensor takes about three lines of code.",{"type":23,"tag":36,"props":1856,"children":1858},{"id":1857},"what-i-changed-and-why",[1859],{"type":28,"value":1860},"What I Changed and Why",{"type":23,"tag":24,"props":1862,"children":1863},{},[1864],{"type":28,"value":1865},"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":23,"tag":24,"props":1867,"children":1868},{},[1869],{"type":28,"value":1870},"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":23,"tag":36,"props":1872,"children":1874},{"id":1873},"the-new-poller-typescript-and-modbus-serial",[1875],{"type":28,"value":1876},"The New Poller: TypeScript and modbus-serial",{"type":23,"tag":24,"props":1878,"children":1879},{},[1880,1882,1888,1890,1895],{"type":28,"value":1881},"The poller was rewritten in TypeScript on Node.js 20. I used ",{"type":23,"tag":131,"props":1883,"children":1885},{"className":1884},[],[1886],{"type":28,"value":1887},"modbus-serial",{"type":28,"value":1889}," for the Modbus RTU communication and the ",{"type":23,"tag":131,"props":1891,"children":1893},{"className":1892},[],[1894],{"type":28,"value":14},{"type":28,"value":1896}," library for publishing. The polling interval stayed at 5 seconds.",{"type":23,"tag":125,"props":1898,"children":1901},{"code":1899,"language":1806,"meta":8,"className":1900,"style":8},"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",[1902],{"type":23,"tag":131,"props":1903,"children":1904},{"__ignoreMap":8},[1905,1933,1958,1965,1999,2032,2049,2066,2083,2100,2108,2133],{"type":23,"tag":135,"props":1906,"children":1907},{"class":137,"line":138},[1908,1913,1918,1923,1928],{"type":23,"tag":135,"props":1909,"children":1910},{"style":820},[1911],{"type":28,"value":1912},"import",{"type":23,"tag":135,"props":1914,"children":1915},{"style":163},[1916],{"type":28,"value":1917}," ModbusRTU ",{"type":23,"tag":135,"props":1919,"children":1920},{"style":820},[1921],{"type":28,"value":1922},"from",{"type":23,"tag":135,"props":1924,"children":1925},{"style":184},[1926],{"type":28,"value":1927}," \"modbus-serial\"",{"type":23,"tag":135,"props":1929,"children":1930},{"style":163},[1931],{"type":28,"value":1932},";\n",{"type":23,"tag":135,"props":1934,"children":1935},{"class":137,"line":169},[1936,1940,1945,1949,1954],{"type":23,"tag":135,"props":1937,"children":1938},{"style":820},[1939],{"type":28,"value":1912},{"type":23,"tag":135,"props":1941,"children":1942},{"style":163},[1943],{"type":28,"value":1944}," mqtt ",{"type":23,"tag":135,"props":1946,"children":1947},{"style":820},[1948],{"type":28,"value":1922},{"type":23,"tag":135,"props":1950,"children":1951},{"style":184},[1952],{"type":28,"value":1953}," \"mqtt\"",{"type":23,"tag":135,"props":1955,"children":1956},{"style":163},[1957],{"type":28,"value":1932},{"type":23,"tag":135,"props":1959,"children":1960},{"class":137,"line":195},[1961],{"type":23,"tag":135,"props":1962,"children":1963},{"emptyLinePlaceholder":1226},[1964],{"type":28,"value":1229},{"type":23,"tag":135,"props":1966,"children":1967},{"class":137,"line":217},[1968,1973,1978,1983,1988,1994],{"type":23,"tag":135,"props":1969,"children":1970},{"style":820},[1971],{"type":28,"value":1972},"const",{"type":23,"tag":135,"props":1974,"children":1975},{"style":173},[1976],{"type":28,"value":1977}," client",{"type":23,"tag":135,"props":1979,"children":1980},{"style":820},[1981],{"type":28,"value":1982}," =",{"type":23,"tag":135,"props":1984,"children":1985},{"style":820},[1986],{"type":28,"value":1987}," new",{"type":23,"tag":135,"props":1989,"children":1991},{"style":1990},"--shiki-default:#B392F0",[1992],{"type":28,"value":1993}," ModbusRTU",{"type":23,"tag":135,"props":1995,"children":1996},{"style":163},[1997],{"type":28,"value":1998},"();\n",{"type":23,"tag":135,"props":2000,"children":2001},{"class":137,"line":239},[2002,2007,2012,2017,2022,2027],{"type":23,"tag":135,"props":2003,"children":2004},{"style":820},[2005],{"type":28,"value":2006},"await",{"type":23,"tag":135,"props":2008,"children":2009},{"style":163},[2010],{"type":28,"value":2011}," client.",{"type":23,"tag":135,"props":2013,"children":2014},{"style":1990},[2015],{"type":28,"value":2016},"connectRTUBuffered",{"type":23,"tag":135,"props":2018,"children":2019},{"style":163},[2020],{"type":28,"value":2021},"(",{"type":23,"tag":135,"props":2023,"children":2024},{"style":184},[2025],{"type":28,"value":2026},"\"/dev/ttyUSB0\"",{"type":23,"tag":135,"props":2028,"children":2029},{"style":163},[2030],{"type":28,"value":2031},", {\n",{"type":23,"tag":135,"props":2033,"children":2034},{"class":137,"line":261},[2035,2040,2045],{"type":23,"tag":135,"props":2036,"children":2037},{"style":163},[2038],{"type":28,"value":2039},"  baudRate: ",{"type":23,"tag":135,"props":2041,"children":2042},{"style":173},[2043],{"type":28,"value":2044},"9600",{"type":23,"tag":135,"props":2046,"children":2047},{"style":163},[2048],{"type":28,"value":192},{"type":23,"tag":135,"props":2050,"children":2051},{"class":137,"line":283},[2052,2057,2062],{"type":23,"tag":135,"props":2053,"children":2054},{"style":163},[2055],{"type":28,"value":2056},"  parity: ",{"type":23,"tag":135,"props":2058,"children":2059},{"style":184},[2060],{"type":28,"value":2061},"\"none\"",{"type":23,"tag":135,"props":2063,"children":2064},{"style":163},[2065],{"type":28,"value":192},{"type":23,"tag":135,"props":2067,"children":2068},{"class":137,"line":305},[2069,2074,2079],{"type":23,"tag":135,"props":2070,"children":2071},{"style":163},[2072],{"type":28,"value":2073},"  stopBits: ",{"type":23,"tag":135,"props":2075,"children":2076},{"style":173},[2077],{"type":28,"value":2078},"1",{"type":23,"tag":135,"props":2080,"children":2081},{"style":163},[2082],{"type":28,"value":192},{"type":23,"tag":135,"props":2084,"children":2085},{"class":137,"line":319},[2086,2091,2096],{"type":23,"tag":135,"props":2087,"children":2088},{"style":163},[2089],{"type":28,"value":2090},"  dataBits: ",{"type":23,"tag":135,"props":2092,"children":2093},{"style":173},[2094],{"type":28,"value":2095},"8",{"type":23,"tag":135,"props":2097,"children":2098},{"style":163},[2099],{"type":28,"value":192},{"type":23,"tag":135,"props":2101,"children":2102},{"class":137,"line":343},[2103],{"type":23,"tag":135,"props":2104,"children":2105},{"style":163},[2106],{"type":28,"value":2107},"});\n",{"type":23,"tag":135,"props":2109,"children":2110},{"class":137,"line":361},[2111,2116,2121,2125,2129],{"type":23,"tag":135,"props":2112,"children":2113},{"style":163},[2114],{"type":28,"value":2115},"client.",{"type":23,"tag":135,"props":2117,"children":2118},{"style":1990},[2119],{"type":28,"value":2120},"setID",{"type":23,"tag":135,"props":2122,"children":2123},{"style":163},[2124],{"type":28,"value":2021},{"type":23,"tag":135,"props":2126,"children":2127},{"style":173},[2128],{"type":28,"value":2078},{"type":23,"tag":135,"props":2130,"children":2131},{"style":163},[2132],{"type":28,"value":1611},{"type":23,"tag":135,"props":2134,"children":2135},{"class":137,"line":370},[2136,2140,2145,2149,2154],{"type":23,"tag":135,"props":2137,"children":2138},{"style":163},[2139],{"type":28,"value":2115},{"type":23,"tag":135,"props":2141,"children":2142},{"style":1990},[2143],{"type":28,"value":2144},"setTimeout",{"type":23,"tag":135,"props":2146,"children":2147},{"style":163},[2148],{"type":28,"value":2021},{"type":23,"tag":135,"props":2150,"children":2151},{"style":173},[2152],{"type":28,"value":2153},"3000",{"type":23,"tag":135,"props":2155,"children":2156},{"style":163},[2157],{"type":28,"value":1611},{"type":23,"tag":24,"props":2159,"children":2160},{},[2161],{"type":28,"value":2162},"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":23,"tag":125,"props":2164,"children":2166},{"code":2165,"language":1806,"meta":8,"className":1900,"style":8},"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",[2167],{"type":23,"tag":131,"props":2168,"children":2169},{"__ignoreMap":8},[2170,2192,2215,2236,2256,2276,2297,2317,2325,2332,2366,2429,2491,2550,2608,2665,2726,2734],{"type":23,"tag":135,"props":2171,"children":2172},{"class":137,"line":138},[2173,2178,2183,2187],{"type":23,"tag":135,"props":2174,"children":2175},{"style":820},[2176],{"type":28,"value":2177},"type",{"type":23,"tag":135,"props":2179,"children":2180},{"style":1990},[2181],{"type":28,"value":2182}," RegisterDef",{"type":23,"tag":135,"props":2184,"children":2185},{"style":820},[2186],{"type":28,"value":1982},{"type":23,"tag":135,"props":2188,"children":2189},{"style":163},[2190],{"type":28,"value":2191}," {\n",{"type":23,"tag":135,"props":2193,"children":2194},{"class":137,"line":169},[2195,2201,2206,2211],{"type":23,"tag":135,"props":2196,"children":2198},{"style":2197},"--shiki-default:#FFAB70",[2199],{"type":28,"value":2200},"  address",{"type":23,"tag":135,"props":2202,"children":2203},{"style":820},[2204],{"type":28,"value":2205},":",{"type":23,"tag":135,"props":2207,"children":2208},{"style":173},[2209],{"type":28,"value":2210}," number",{"type":23,"tag":135,"props":2212,"children":2213},{"style":163},[2214],{"type":28,"value":1932},{"type":23,"tag":135,"props":2216,"children":2217},{"class":137,"line":195},[2218,2223,2227,2232],{"type":23,"tag":135,"props":2219,"children":2220},{"style":2197},[2221],{"type":28,"value":2222},"  name",{"type":23,"tag":135,"props":2224,"children":2225},{"style":820},[2226],{"type":28,"value":2205},{"type":23,"tag":135,"props":2228,"children":2229},{"style":173},[2230],{"type":28,"value":2231}," string",{"type":23,"tag":135,"props":2233,"children":2234},{"style":163},[2235],{"type":28,"value":1932},{"type":23,"tag":135,"props":2237,"children":2238},{"class":137,"line":217},[2239,2244,2248,2252],{"type":23,"tag":135,"props":2240,"children":2241},{"style":2197},[2242],{"type":28,"value":2243},"  scale",{"type":23,"tag":135,"props":2245,"children":2246},{"style":820},[2247],{"type":28,"value":2205},{"type":23,"tag":135,"props":2249,"children":2250},{"style":173},[2251],{"type":28,"value":2210},{"type":23,"tag":135,"props":2253,"children":2254},{"style":163},[2255],{"type":28,"value":1932},{"type":23,"tag":135,"props":2257,"children":2258},{"class":137,"line":239},[2259,2264,2268,2272],{"type":23,"tag":135,"props":2260,"children":2261},{"style":2197},[2262],{"type":28,"value":2263},"  unit",{"type":23,"tag":135,"props":2265,"children":2266},{"style":820},[2267],{"type":28,"value":2205},{"type":23,"tag":135,"props":2269,"children":2270},{"style":173},[2271],{"type":28,"value":2231},{"type":23,"tag":135,"props":2273,"children":2274},{"style":163},[2275],{"type":28,"value":1932},{"type":23,"tag":135,"props":2277,"children":2278},{"class":137,"line":261},[2279,2284,2289,2293],{"type":23,"tag":135,"props":2280,"children":2281},{"style":2197},[2282],{"type":28,"value":2283},"  deviceClass",{"type":23,"tag":135,"props":2285,"children":2286},{"style":820},[2287],{"type":28,"value":2288},"?:",{"type":23,"tag":135,"props":2290,"children":2291},{"style":173},[2292],{"type":28,"value":2231},{"type":23,"tag":135,"props":2294,"children":2295},{"style":163},[2296],{"type":28,"value":1932},{"type":23,"tag":135,"props":2298,"children":2299},{"class":137,"line":283},[2300,2305,2309,2313],{"type":23,"tag":135,"props":2301,"children":2302},{"style":2197},[2303],{"type":28,"value":2304},"  stateClass",{"type":23,"tag":135,"props":2306,"children":2307},{"style":820},[2308],{"type":28,"value":2288},{"type":23,"tag":135,"props":2310,"children":2311},{"style":173},[2312],{"type":28,"value":2231},{"type":23,"tag":135,"props":2314,"children":2315},{"style":163},[2316],{"type":28,"value":1932},{"type":23,"tag":135,"props":2318,"children":2319},{"class":137,"line":305},[2320],{"type":23,"tag":135,"props":2321,"children":2322},{"style":163},[2323],{"type":28,"value":2324},"};\n",{"type":23,"tag":135,"props":2326,"children":2327},{"class":137,"line":319},[2328],{"type":23,"tag":135,"props":2329,"children":2330},{"emptyLinePlaceholder":1226},[2331],{"type":28,"value":1229},{"type":23,"tag":135,"props":2333,"children":2334},{"class":137,"line":343},[2335,2339,2344,2348,2352,2357,2361],{"type":23,"tag":135,"props":2336,"children":2337},{"style":820},[2338],{"type":28,"value":1972},{"type":23,"tag":135,"props":2340,"children":2341},{"style":173},[2342],{"type":28,"value":2343}," REGISTERS",{"type":23,"tag":135,"props":2345,"children":2346},{"style":820},[2347],{"type":28,"value":2205},{"type":23,"tag":135,"props":2349,"children":2350},{"style":1990},[2351],{"type":28,"value":2182},{"type":23,"tag":135,"props":2353,"children":2354},{"style":163},[2355],{"type":28,"value":2356},"[] ",{"type":23,"tag":135,"props":2358,"children":2359},{"style":820},[2360],{"type":28,"value":823},{"type":23,"tag":135,"props":2362,"children":2363},{"style":163},[2364],{"type":28,"value":2365}," [\n",{"type":23,"tag":135,"props":2367,"children":2368},{"class":137,"line":361},[2369,2374,2379,2384,2388,2393,2397,2402,2406,2411,2415,2420,2424],{"type":23,"tag":135,"props":2370,"children":2371},{"style":163},[2372],{"type":28,"value":2373},"  { address: ",{"type":23,"tag":135,"props":2375,"children":2376},{"style":173},[2377],{"type":28,"value":2378},"0x0100",{"type":23,"tag":135,"props":2380,"children":2381},{"style":163},[2382],{"type":28,"value":2383},", name: ",{"type":23,"tag":135,"props":2385,"children":2386},{"style":184},[2387],{"type":28,"value":187},{"type":23,"tag":135,"props":2389,"children":2390},{"style":163},[2391],{"type":28,"value":2392},",     scale: ",{"type":23,"tag":135,"props":2394,"children":2395},{"style":173},[2396],{"type":28,"value":2078},{"type":23,"tag":135,"props":2398,"children":2399},{"style":163},[2400],{"type":28,"value":2401},",    unit: ",{"type":23,"tag":135,"props":2403,"children":2404},{"style":184},[2405],{"type":28,"value":232},{"type":23,"tag":135,"props":2407,"children":2408},{"style":163},[2409],{"type":28,"value":2410},",  deviceClass: ",{"type":23,"tag":135,"props":2412,"children":2413},{"style":184},[2414],{"type":28,"value":254},{"type":23,"tag":135,"props":2416,"children":2417},{"style":163},[2418],{"type":28,"value":2419},",     stateClass: ",{"type":23,"tag":135,"props":2421,"children":2422},{"style":184},[2423],{"type":28,"value":276},{"type":23,"tag":135,"props":2425,"children":2426},{"style":163},[2427],{"type":28,"value":2428}," },\n",{"type":23,"tag":135,"props":2430,"children":2431},{"class":137,"line":370},[2432,2436,2441,2445,2450,2455,2460,2465,2470,2474,2479,2483,2487],{"type":23,"tag":135,"props":2433,"children":2434},{"style":163},[2435],{"type":28,"value":2373},{"type":23,"tag":135,"props":2437,"children":2438},{"style":173},[2439],{"type":28,"value":2440},"0x0101",{"type":23,"tag":135,"props":2442,"children":2443},{"style":163},[2444],{"type":28,"value":2383},{"type":23,"tag":135,"props":2446,"children":2447},{"style":184},[2448],{"type":28,"value":2449},"\"battery_voltage\"",{"type":23,"tag":135,"props":2451,"children":2452},{"style":163},[2453],{"type":28,"value":2454},",  scale: ",{"type":23,"tag":135,"props":2456,"children":2457},{"style":173},[2458],{"type":28,"value":2459},"0.1",{"type":23,"tag":135,"props":2461,"children":2462},{"style":163},[2463],{"type":28,"value":2464},",  unit: ",{"type":23,"tag":135,"props":2466,"children":2467},{"style":184},[2468],{"type":28,"value":2469},"\"V\"",{"type":23,"tag":135,"props":2471,"children":2472},{"style":163},[2473],{"type":28,"value":2410},{"type":23,"tag":135,"props":2475,"children":2476},{"style":184},[2477],{"type":28,"value":2478},"\"voltage\"",{"type":23,"tag":135,"props":2480,"children":2481},{"style":163},[2482],{"type":28,"value":2419},{"type":23,"tag":135,"props":2484,"children":2485},{"style":184},[2486],{"type":28,"value":276},{"type":23,"tag":135,"props":2488,"children":2489},{"style":163},[2490],{"type":28,"value":2428},{"type":23,"tag":135,"props":2492,"children":2493},{"class":137,"line":1311},[2494,2498,2503,2507,2512,2516,2520,2524,2529,2533,2538,2542,2546],{"type":23,"tag":135,"props":2495,"children":2496},{"style":163},[2497],{"type":28,"value":2373},{"type":23,"tag":135,"props":2499,"children":2500},{"style":173},[2501],{"type":28,"value":2502},"0x0102",{"type":23,"tag":135,"props":2504,"children":2505},{"style":163},[2506],{"type":28,"value":2383},{"type":23,"tag":135,"props":2508,"children":2509},{"style":184},[2510],{"type":28,"value":2511},"\"battery_current\"",{"type":23,"tag":135,"props":2513,"children":2514},{"style":163},[2515],{"type":28,"value":2454},{"type":23,"tag":135,"props":2517,"children":2518},{"style":173},[2519],{"type":28,"value":2459},{"type":23,"tag":135,"props":2521,"children":2522},{"style":163},[2523],{"type":28,"value":2464},{"type":23,"tag":135,"props":2525,"children":2526},{"style":184},[2527],{"type":28,"value":2528},"\"A\"",{"type":23,"tag":135,"props":2530,"children":2531},{"style":163},[2532],{"type":28,"value":2410},{"type":23,"tag":135,"props":2534,"children":2535},{"style":184},[2536],{"type":28,"value":2537},"\"current\"",{"type":23,"tag":135,"props":2539,"children":2540},{"style":163},[2541],{"type":28,"value":2419},{"type":23,"tag":135,"props":2543,"children":2544},{"style":184},[2545],{"type":28,"value":276},{"type":23,"tag":135,"props":2547,"children":2548},{"style":163},[2549],{"type":28,"value":2428},{"type":23,"tag":135,"props":2551,"children":2552},{"class":137,"line":1320},[2553,2557,2562,2566,2571,2576,2580,2584,2588,2592,2596,2600,2604],{"type":23,"tag":135,"props":2554,"children":2555},{"style":163},[2556],{"type":28,"value":2373},{"type":23,"tag":135,"props":2558,"children":2559},{"style":173},[2560],{"type":28,"value":2561},"0x0107",{"type":23,"tag":135,"props":2563,"children":2564},{"style":163},[2565],{"type":28,"value":2383},{"type":23,"tag":135,"props":2567,"children":2568},{"style":184},[2569],{"type":28,"value":2570},"\"pv1_voltage\"",{"type":23,"tag":135,"props":2572,"children":2573},{"style":163},[2574],{"type":28,"value":2575},",      scale: ",{"type":23,"tag":135,"props":2577,"children":2578},{"style":173},[2579],{"type":28,"value":2459},{"type":23,"tag":135,"props":2581,"children":2582},{"style":163},[2583],{"type":28,"value":2464},{"type":23,"tag":135,"props":2585,"children":2586},{"style":184},[2587],{"type":28,"value":2469},{"type":23,"tag":135,"props":2589,"children":2590},{"style":163},[2591],{"type":28,"value":2410},{"type":23,"tag":135,"props":2593,"children":2594},{"style":184},[2595],{"type":28,"value":2478},{"type":23,"tag":135,"props":2597,"children":2598},{"style":163},[2599],{"type":28,"value":2419},{"type":23,"tag":135,"props":2601,"children":2602},{"style":184},[2603],{"type":28,"value":276},{"type":23,"tag":135,"props":2605,"children":2606},{"style":163},[2607],{"type":28,"value":2428},{"type":23,"tag":135,"props":2609,"children":2610},{"class":137,"line":1329},[2611,2615,2620,2624,2629,2633,2637,2641,2645,2649,2653,2657,2661],{"type":23,"tag":135,"props":2612,"children":2613},{"style":163},[2614],{"type":28,"value":2373},{"type":23,"tag":135,"props":2616,"children":2617},{"style":173},[2618],{"type":28,"value":2619},"0x0108",{"type":23,"tag":135,"props":2621,"children":2622},{"style":163},[2623],{"type":28,"value":2383},{"type":23,"tag":135,"props":2625,"children":2626},{"style":184},[2627],{"type":28,"value":2628},"\"pv1_current\"",{"type":23,"tag":135,"props":2630,"children":2631},{"style":163},[2632],{"type":28,"value":2575},{"type":23,"tag":135,"props":2634,"children":2635},{"style":173},[2636],{"type":28,"value":2459},{"type":23,"tag":135,"props":2638,"children":2639},{"style":163},[2640],{"type":28,"value":2464},{"type":23,"tag":135,"props":2642,"children":2643},{"style":184},[2644],{"type":28,"value":2528},{"type":23,"tag":135,"props":2646,"children":2647},{"style":163},[2648],{"type":28,"value":2410},{"type":23,"tag":135,"props":2650,"children":2651},{"style":184},[2652],{"type":28,"value":2537},{"type":23,"tag":135,"props":2654,"children":2655},{"style":163},[2656],{"type":28,"value":2419},{"type":23,"tag":135,"props":2658,"children":2659},{"style":184},[2660],{"type":28,"value":276},{"type":23,"tag":135,"props":2662,"children":2663},{"style":163},[2664],{"type":28,"value":2428},{"type":23,"tag":135,"props":2666,"children":2667},{"class":137,"line":1338},[2668,2672,2677,2681,2686,2691,2695,2699,2704,2708,2713,2718,2722],{"type":23,"tag":135,"props":2669,"children":2670},{"style":163},[2671],{"type":28,"value":2373},{"type":23,"tag":135,"props":2673,"children":2674},{"style":173},[2675],{"type":28,"value":2676},"0x0109",{"type":23,"tag":135,"props":2678,"children":2679},{"style":163},[2680],{"type":28,"value":2383},{"type":23,"tag":135,"props":2682,"children":2683},{"style":184},[2684],{"type":28,"value":2685},"\"pv1_power\"",{"type":23,"tag":135,"props":2687,"children":2688},{"style":163},[2689],{"type":28,"value":2690},",        scale: ",{"type":23,"tag":135,"props":2692,"children":2693},{"style":173},[2694],{"type":28,"value":2078},{"type":23,"tag":135,"props":2696,"children":2697},{"style":163},[2698],{"type":28,"value":2401},{"type":23,"tag":135,"props":2700,"children":2701},{"style":184},[2702],{"type":28,"value":2703},"\"W\"",{"type":23,"tag":135,"props":2705,"children":2706},{"style":163},[2707],{"type":28,"value":2410},{"type":23,"tag":135,"props":2709,"children":2710},{"style":184},[2711],{"type":28,"value":2712},"\"power\"",{"type":23,"tag":135,"props":2714,"children":2715},{"style":163},[2716],{"type":28,"value":2717},",       stateClass: ",{"type":23,"tag":135,"props":2719,"children":2720},{"style":184},[2721],{"type":28,"value":276},{"type":23,"tag":135,"props":2723,"children":2724},{"style":163},[2725],{"type":28,"value":2428},{"type":23,"tag":135,"props":2727,"children":2728},{"class":137,"line":1347},[2729],{"type":23,"tag":135,"props":2730,"children":2731},{"style":674},[2732],{"type":28,"value":2733},"  // ...~24 more\n",{"type":23,"tag":135,"props":2735,"children":2736},{"class":137,"line":1355},[2737],{"type":23,"tag":135,"props":2738,"children":2739},{"style":163},[2740],{"type":28,"value":2741},"];\n",{"type":23,"tag":24,"props":2743,"children":2744},{},[2745],{"type":28,"value":2746},"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":23,"tag":1377,"props":2748,"children":2750},{"id":2749},"auto-grouping-contiguous-registers",[2751],{"type":28,"value":2752},"Auto-Grouping Contiguous Registers",{"type":23,"tag":24,"props":2754,"children":2755},{},[2756],{"type":28,"value":2757},"The same contiguous grouping logic from the Python version is here, but typed:",{"type":23,"tag":125,"props":2759,"children":2761},{"code":2760,"language":1806,"meta":8,"className":1900,"style":8},"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",[2762],{"type":23,"tag":131,"props":2763,"children":2764},{"__ignoreMap":8},[2765,2867,2948,3030,3074,3081,3148,3197,3215,3232,3267,3284,3292,3299,3331,3344],{"type":23,"tag":135,"props":2766,"children":2767},{"class":137,"line":138},[2768,2773,2778,2782,2787,2791,2795,2800,2804,2809,2814,2819,2823,2827,2832,2837,2841,2845,2849,2854,2858,2862],{"type":23,"tag":135,"props":2769,"children":2770},{"style":820},[2771],{"type":28,"value":2772},"function",{"type":23,"tag":135,"props":2774,"children":2775},{"style":1990},[2776],{"type":28,"value":2777}," groupContiguous",{"type":23,"tag":135,"props":2779,"children":2780},{"style":163},[2781],{"type":28,"value":2021},{"type":23,"tag":135,"props":2783,"children":2784},{"style":2197},[2785],{"type":28,"value":2786},"regs",{"type":23,"tag":135,"props":2788,"children":2789},{"style":820},[2790],{"type":28,"value":2205},{"type":23,"tag":135,"props":2792,"children":2793},{"style":1990},[2794],{"type":28,"value":2182},{"type":23,"tag":135,"props":2796,"children":2797},{"style":163},[2798],{"type":28,"value":2799},"[])",{"type":23,"tag":135,"props":2801,"children":2802},{"style":820},[2803],{"type":28,"value":2205},{"type":23,"tag":135,"props":2805,"children":2806},{"style":1990},[2807],{"type":28,"value":2808}," Array",{"type":23,"tag":135,"props":2810,"children":2811},{"style":163},[2812],{"type":28,"value":2813},"\u003C{ ",{"type":23,"tag":135,"props":2815,"children":2816},{"style":2197},[2817],{"type":28,"value":2818},"start",{"type":23,"tag":135,"props":2820,"children":2821},{"style":820},[2822],{"type":28,"value":2205},{"type":23,"tag":135,"props":2824,"children":2825},{"style":173},[2826],{"type":28,"value":2210},{"type":23,"tag":135,"props":2828,"children":2829},{"style":163},[2830],{"type":28,"value":2831},"; ",{"type":23,"tag":135,"props":2833,"children":2834},{"style":2197},[2835],{"type":28,"value":2836},"count",{"type":23,"tag":135,"props":2838,"children":2839},{"style":820},[2840],{"type":28,"value":2205},{"type":23,"tag":135,"props":2842,"children":2843},{"style":173},[2844],{"type":28,"value":2210},{"type":23,"tag":135,"props":2846,"children":2847},{"style":163},[2848],{"type":28,"value":2831},{"type":23,"tag":135,"props":2850,"children":2851},{"style":2197},[2852],{"type":28,"value":2853},"defs",{"type":23,"tag":135,"props":2855,"children":2856},{"style":820},[2857],{"type":28,"value":2205},{"type":23,"tag":135,"props":2859,"children":2860},{"style":1990},[2861],{"type":28,"value":2182},{"type":23,"tag":135,"props":2863,"children":2864},{"style":163},[2865],{"type":28,"value":2866},"[] }> {\n",{"type":23,"tag":135,"props":2868,"children":2869},{"class":137,"line":169},[2870,2875,2880,2884,2889,2894,2899,2904,2909,2913,2918,2923,2928,2933,2938,2943],{"type":23,"tag":135,"props":2871,"children":2872},{"style":820},[2873],{"type":28,"value":2874},"  const",{"type":23,"tag":135,"props":2876,"children":2877},{"style":173},[2878],{"type":28,"value":2879}," sorted",{"type":23,"tag":135,"props":2881,"children":2882},{"style":820},[2883],{"type":28,"value":1982},{"type":23,"tag":135,"props":2885,"children":2886},{"style":163},[2887],{"type":28,"value":2888}," [",{"type":23,"tag":135,"props":2890,"children":2891},{"style":820},[2892],{"type":28,"value":2893},"...",{"type":23,"tag":135,"props":2895,"children":2896},{"style":163},[2897],{"type":28,"value":2898},"regs].",{"type":23,"tag":135,"props":2900,"children":2901},{"style":1990},[2902],{"type":28,"value":2903},"sort",{"type":23,"tag":135,"props":2905,"children":2906},{"style":163},[2907],{"type":28,"value":2908},"((",{"type":23,"tag":135,"props":2910,"children":2911},{"style":2197},[2912],{"type":28,"value":48},{"type":23,"tag":135,"props":2914,"children":2915},{"style":163},[2916],{"type":28,"value":2917},", ",{"type":23,"tag":135,"props":2919,"children":2920},{"style":2197},[2921],{"type":28,"value":2922},"b",{"type":23,"tag":135,"props":2924,"children":2925},{"style":163},[2926],{"type":28,"value":2927},") ",{"type":23,"tag":135,"props":2929,"children":2930},{"style":820},[2931],{"type":28,"value":2932},"=>",{"type":23,"tag":135,"props":2934,"children":2935},{"style":163},[2936],{"type":28,"value":2937}," a.address ",{"type":23,"tag":135,"props":2939,"children":2940},{"style":820},[2941],{"type":28,"value":2942},"-",{"type":23,"tag":135,"props":2944,"children":2945},{"style":163},[2946],{"type":28,"value":2947}," b.address);\n",{"type":23,"tag":135,"props":2949,"children":2950},{"class":137,"line":195},[2951,2955,2960,2964,2968,2972,2976,2980,2984,2988,2992,2996,3000,3004,3008,3012,3016,3021,3025],{"type":23,"tag":135,"props":2952,"children":2953},{"style":820},[2954],{"type":28,"value":2874},{"type":23,"tag":135,"props":2956,"children":2957},{"style":173},[2958],{"type":28,"value":2959}," blocks",{"type":23,"tag":135,"props":2961,"children":2962},{"style":820},[2963],{"type":28,"value":2205},{"type":23,"tag":135,"props":2965,"children":2966},{"style":1990},[2967],{"type":28,"value":2808},{"type":23,"tag":135,"props":2969,"children":2970},{"style":163},[2971],{"type":28,"value":2813},{"type":23,"tag":135,"props":2973,"children":2974},{"style":2197},[2975],{"type":28,"value":2818},{"type":23,"tag":135,"props":2977,"children":2978},{"style":820},[2979],{"type":28,"value":2205},{"type":23,"tag":135,"props":2981,"children":2982},{"style":173},[2983],{"type":28,"value":2210},{"type":23,"tag":135,"props":2985,"children":2986},{"style":163},[2987],{"type":28,"value":2831},{"type":23,"tag":135,"props":2989,"children":2990},{"style":2197},[2991],{"type":28,"value":2836},{"type":23,"tag":135,"props":2993,"children":2994},{"style":820},[2995],{"type":28,"value":2205},{"type":23,"tag":135,"props":2997,"children":2998},{"style":173},[2999],{"type":28,"value":2210},{"type":23,"tag":135,"props":3001,"children":3002},{"style":163},[3003],{"type":28,"value":2831},{"type":23,"tag":135,"props":3005,"children":3006},{"style":2197},[3007],{"type":28,"value":2853},{"type":23,"tag":135,"props":3009,"children":3010},{"style":820},[3011],{"type":28,"value":2205},{"type":23,"tag":135,"props":3013,"children":3014},{"style":1990},[3015],{"type":28,"value":2182},{"type":23,"tag":135,"props":3017,"children":3018},{"style":163},[3019],{"type":28,"value":3020},"[] }> ",{"type":23,"tag":135,"props":3022,"children":3023},{"style":820},[3024],{"type":28,"value":823},{"type":23,"tag":135,"props":3026,"children":3027},{"style":163},[3028],{"type":28,"value":3029}," [];\n",{"type":23,"tag":135,"props":3031,"children":3032},{"class":137,"line":217},[3033,3038,3043,3047,3051,3055,3059,3064,3069],{"type":23,"tag":135,"props":3034,"children":3035},{"style":820},[3036],{"type":28,"value":3037},"  let",{"type":23,"tag":135,"props":3039,"children":3040},{"style":163},[3041],{"type":28,"value":3042}," group",{"type":23,"tag":135,"props":3044,"children":3045},{"style":820},[3046],{"type":28,"value":2205},{"type":23,"tag":135,"props":3048,"children":3049},{"style":1990},[3050],{"type":28,"value":2182},{"type":23,"tag":135,"props":3052,"children":3053},{"style":163},[3054],{"type":28,"value":2356},{"type":23,"tag":135,"props":3056,"children":3057},{"style":820},[3058],{"type":28,"value":823},{"type":23,"tag":135,"props":3060,"children":3061},{"style":163},[3062],{"type":28,"value":3063}," [sorted[",{"type":23,"tag":135,"props":3065,"children":3066},{"style":173},[3067],{"type":28,"value":3068},"0",{"type":23,"tag":135,"props":3070,"children":3071},{"style":163},[3072],{"type":28,"value":3073},"]];\n",{"type":23,"tag":135,"props":3075,"children":3076},{"class":137,"line":239},[3077],{"type":23,"tag":135,"props":3078,"children":3079},{"emptyLinePlaceholder":1226},[3080],{"type":28,"value":1229},{"type":23,"tag":135,"props":3082,"children":3083},{"class":137,"line":261},[3084,3089,3094,3099,3104,3108,3113,3118,3123,3128,3133,3138,3143],{"type":23,"tag":135,"props":3085,"children":3086},{"style":820},[3087],{"type":28,"value":3088},"  for",{"type":23,"tag":135,"props":3090,"children":3091},{"style":163},[3092],{"type":28,"value":3093}," (",{"type":23,"tag":135,"props":3095,"children":3096},{"style":820},[3097],{"type":28,"value":3098},"let",{"type":23,"tag":135,"props":3100,"children":3101},{"style":163},[3102],{"type":28,"value":3103}," i ",{"type":23,"tag":135,"props":3105,"children":3106},{"style":820},[3107],{"type":28,"value":823},{"type":23,"tag":135,"props":3109,"children":3110},{"style":173},[3111],{"type":28,"value":3112}," 1",{"type":23,"tag":135,"props":3114,"children":3115},{"style":163},[3116],{"type":28,"value":3117},"; i ",{"type":23,"tag":135,"props":3119,"children":3120},{"style":820},[3121],{"type":28,"value":3122},"\u003C",{"type":23,"tag":135,"props":3124,"children":3125},{"style":163},[3126],{"type":28,"value":3127}," sorted.",{"type":23,"tag":135,"props":3129,"children":3130},{"style":173},[3131],{"type":28,"value":3132},"length",{"type":23,"tag":135,"props":3134,"children":3135},{"style":163},[3136],{"type":28,"value":3137},"; i",{"type":23,"tag":135,"props":3139,"children":3140},{"style":820},[3141],{"type":28,"value":3142},"++",{"type":23,"tag":135,"props":3144,"children":3145},{"style":163},[3146],{"type":28,"value":3147},") {\n",{"type":23,"tag":135,"props":3149,"children":3150},{"class":137,"line":283},[3151,3156,3161,3166,3171,3175,3179,3184,3189,3193],{"type":23,"tag":135,"props":3152,"children":3153},{"style":820},[3154],{"type":28,"value":3155},"    if",{"type":23,"tag":135,"props":3157,"children":3158},{"style":163},[3159],{"type":28,"value":3160}," (sorted[i].address ",{"type":23,"tag":135,"props":3162,"children":3163},{"style":820},[3164],{"type":28,"value":3165},"===",{"type":23,"tag":135,"props":3167,"children":3168},{"style":163},[3169],{"type":28,"value":3170}," sorted[i ",{"type":23,"tag":135,"props":3172,"children":3173},{"style":820},[3174],{"type":28,"value":2942},{"type":23,"tag":135,"props":3176,"children":3177},{"style":173},[3178],{"type":28,"value":3112},{"type":23,"tag":135,"props":3180,"children":3181},{"style":163},[3182],{"type":28,"value":3183},"].address ",{"type":23,"tag":135,"props":3185,"children":3186},{"style":820},[3187],{"type":28,"value":3188},"+",{"type":23,"tag":135,"props":3190,"children":3191},{"style":173},[3192],{"type":28,"value":3112},{"type":23,"tag":135,"props":3194,"children":3195},{"style":163},[3196],{"type":28,"value":3147},{"type":23,"tag":135,"props":3198,"children":3199},{"class":137,"line":305},[3200,3205,3210],{"type":23,"tag":135,"props":3201,"children":3202},{"style":163},[3203],{"type":28,"value":3204},"      group.",{"type":23,"tag":135,"props":3206,"children":3207},{"style":1990},[3208],{"type":28,"value":3209},"push",{"type":23,"tag":135,"props":3211,"children":3212},{"style":163},[3213],{"type":28,"value":3214},"(sorted[i]);\n",{"type":23,"tag":135,"props":3216,"children":3217},{"class":137,"line":319},[3218,3223,3228],{"type":23,"tag":135,"props":3219,"children":3220},{"style":163},[3221],{"type":28,"value":3222},"    } ",{"type":23,"tag":135,"props":3224,"children":3225},{"style":820},[3226],{"type":28,"value":3227},"else",{"type":23,"tag":135,"props":3229,"children":3230},{"style":163},[3231],{"type":28,"value":2191},{"type":23,"tag":135,"props":3233,"children":3234},{"class":137,"line":343},[3235,3240,3244,3249,3253,3258,3262],{"type":23,"tag":135,"props":3236,"children":3237},{"style":163},[3238],{"type":28,"value":3239},"      blocks.",{"type":23,"tag":135,"props":3241,"children":3242},{"style":1990},[3243],{"type":28,"value":3209},{"type":23,"tag":135,"props":3245,"children":3246},{"style":163},[3247],{"type":28,"value":3248},"({ start: group[",{"type":23,"tag":135,"props":3250,"children":3251},{"style":173},[3252],{"type":28,"value":3068},{"type":23,"tag":135,"props":3254,"children":3255},{"style":163},[3256],{"type":28,"value":3257},"].address, count: group.",{"type":23,"tag":135,"props":3259,"children":3260},{"style":173},[3261],{"type":28,"value":3132},{"type":23,"tag":135,"props":3263,"children":3264},{"style":163},[3265],{"type":28,"value":3266},", defs: group });\n",{"type":23,"tag":135,"props":3268,"children":3269},{"class":137,"line":361},[3270,3275,3279],{"type":23,"tag":135,"props":3271,"children":3272},{"style":163},[3273],{"type":28,"value":3274},"      group ",{"type":23,"tag":135,"props":3276,"children":3277},{"style":820},[3278],{"type":28,"value":823},{"type":23,"tag":135,"props":3280,"children":3281},{"style":163},[3282],{"type":28,"value":3283}," [sorted[i]];\n",{"type":23,"tag":135,"props":3285,"children":3286},{"class":137,"line":370},[3287],{"type":23,"tag":135,"props":3288,"children":3289},{"style":163},[3290],{"type":28,"value":3291},"    }\n",{"type":23,"tag":135,"props":3293,"children":3294},{"class":137,"line":1311},[3295],{"type":23,"tag":135,"props":3296,"children":3297},{"style":163},[3298],{"type":28,"value":367},{"type":23,"tag":135,"props":3300,"children":3301},{"class":137,"line":1320},[3302,3307,3311,3315,3319,3323,3327],{"type":23,"tag":135,"props":3303,"children":3304},{"style":163},[3305],{"type":28,"value":3306},"  blocks.",{"type":23,"tag":135,"props":3308,"children":3309},{"style":1990},[3310],{"type":28,"value":3209},{"type":23,"tag":135,"props":3312,"children":3313},{"style":163},[3314],{"type":28,"value":3248},{"type":23,"tag":135,"props":3316,"children":3317},{"style":173},[3318],{"type":28,"value":3068},{"type":23,"tag":135,"props":3320,"children":3321},{"style":163},[3322],{"type":28,"value":3257},{"type":23,"tag":135,"props":3324,"children":3325},{"style":173},[3326],{"type":28,"value":3132},{"type":23,"tag":135,"props":3328,"children":3329},{"style":163},[3330],{"type":28,"value":3266},{"type":23,"tag":135,"props":3332,"children":3333},{"class":137,"line":1329},[3334,3339],{"type":23,"tag":135,"props":3335,"children":3336},{"style":820},[3337],{"type":28,"value":3338},"  return",{"type":23,"tag":135,"props":3340,"children":3341},{"style":163},[3342],{"type":28,"value":3343}," blocks;\n",{"type":23,"tag":135,"props":3345,"children":3346},{"class":137,"line":1338},[3347],{"type":23,"tag":135,"props":3348,"children":3349},{"style":163},[3350],{"type":28,"value":376},{"type":23,"tag":24,"props":3352,"children":3353},{},[3354],{"type":28,"value":3355},"This runs once at startup, so there's no overhead per poll cycle.",{"type":23,"tag":36,"props":3357,"children":3359},{"id":3358},"mqtt-replaces-the-custom-api",[3360],{"type":28,"value":3361},"MQTT Replaces the Custom API",{"type":23,"tag":24,"props":3363,"children":3364},{},[3365],{"type":28,"value":3366},"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":23,"tag":24,"props":3368,"children":3369},{},[3370],{"type":28,"value":3371},"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":23,"tag":125,"props":3373,"children":3375},{"code":3374},"solar/battery_soc        → \"87\"\nsolar/battery_voltage    → \"52.3\"\nsolar/pv1_power          → \"1840\"\nsolar/inverter_state     → \"charging\"\n",[3376],{"type":23,"tag":131,"props":3377,"children":3378},{"__ignoreMap":8},[3379],{"type":28,"value":3374},{"type":23,"tag":24,"props":3381,"children":3382},{},[3383],{"type":28,"value":3384},"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":23,"tag":36,"props":3386,"children":3388},{"id":3387},"home-assistant-auto-discovery",[3389],{"type":28,"value":3390},"Home Assistant Auto-Discovery",{"type":23,"tag":24,"props":3392,"children":3393},{},[3394],{"type":28,"value":3395},"This is the part that changed the maintenance picture most dramatically.",{"type":23,"tag":24,"props":3397,"children":3398},{},[3399],{"type":28,"value":3400},"The poller publishes Home Assistant MQTT discovery configs at startup. Each sensor definition maps directly to a discovery message:",{"type":23,"tag":125,"props":3402,"children":3404},{"code":3403,"language":1806,"meta":8,"className":1900,"style":8},"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",[3405],{"type":23,"tag":131,"props":3406,"children":3407},{"__ignoreMap":8},[3408,3482,3502,3553,3588,3596,3604,3612,3645,3653,3669,3686,3703,3720,3728,3736,3743,3761,3790,3812,3831,3840],{"type":23,"tag":135,"props":3409,"children":3410},{"class":137,"line":138},[3411,3415,3420,3424,3429,3433,3438,3442,3447,3451,3456,3460,3464,3469,3473,3478],{"type":23,"tag":135,"props":3412,"children":3413},{"style":820},[3414],{"type":28,"value":2772},{"type":23,"tag":135,"props":3416,"children":3417},{"style":1990},[3418],{"type":28,"value":3419}," publishDiscovery",{"type":23,"tag":135,"props":3421,"children":3422},{"style":163},[3423],{"type":28,"value":2021},{"type":23,"tag":135,"props":3425,"children":3426},{"style":2197},[3427],{"type":28,"value":3428},"mqttClient",{"type":23,"tag":135,"props":3430,"children":3431},{"style":820},[3432],{"type":28,"value":2205},{"type":23,"tag":135,"props":3434,"children":3435},{"style":1990},[3436],{"type":28,"value":3437}," mqtt",{"type":23,"tag":135,"props":3439,"children":3440},{"style":163},[3441],{"type":28,"value":1783},{"type":23,"tag":135,"props":3443,"children":3444},{"style":1990},[3445],{"type":28,"value":3446},"MqttClient",{"type":23,"tag":135,"props":3448,"children":3449},{"style":163},[3450],{"type":28,"value":2917},{"type":23,"tag":135,"props":3452,"children":3453},{"style":2197},[3454],{"type":28,"value":3455},"reg",{"type":23,"tag":135,"props":3457,"children":3458},{"style":820},[3459],{"type":28,"value":2205},{"type":23,"tag":135,"props":3461,"children":3462},{"style":1990},[3463],{"type":28,"value":2182},{"type":23,"tag":135,"props":3465,"children":3466},{"style":163},[3467],{"type":28,"value":3468},")",{"type":23,"tag":135,"props":3470,"children":3471},{"style":820},[3472],{"type":28,"value":2205},{"type":23,"tag":135,"props":3474,"children":3475},{"style":173},[3476],{"type":28,"value":3477}," void",{"type":23,"tag":135,"props":3479,"children":3480},{"style":163},[3481],{"type":28,"value":2191},{"type":23,"tag":135,"props":3483,"children":3484},{"class":137,"line":169},[3485,3489,3494,3498],{"type":23,"tag":135,"props":3486,"children":3487},{"style":820},[3488],{"type":28,"value":2874},{"type":23,"tag":135,"props":3490,"children":3491},{"style":173},[3492],{"type":28,"value":3493}," config",{"type":23,"tag":135,"props":3495,"children":3496},{"style":820},[3497],{"type":28,"value":1982},{"type":23,"tag":135,"props":3499,"children":3500},{"style":163},[3501],{"type":28,"value":2191},{"type":23,"tag":135,"props":3503,"children":3504},{"class":137,"line":195},[3505,3510,3515,3519,3524,3530,3534,3539,3543,3548],{"type":23,"tag":135,"props":3506,"children":3507},{"style":163},[3508],{"type":28,"value":3509},"    name: reg.name.",{"type":23,"tag":135,"props":3511,"children":3512},{"style":1990},[3513],{"type":28,"value":3514},"replace",{"type":23,"tag":135,"props":3516,"children":3517},{"style":163},[3518],{"type":28,"value":2021},{"type":23,"tag":135,"props":3520,"children":3521},{"style":184},[3522],{"type":28,"value":3523},"/",{"type":23,"tag":135,"props":3525,"children":3527},{"style":3526},"--shiki-default:#DBEDFF",[3528],{"type":28,"value":3529},"_",{"type":23,"tag":135,"props":3531,"children":3532},{"style":184},[3533],{"type":28,"value":3523},{"type":23,"tag":135,"props":3535,"children":3536},{"style":820},[3537],{"type":28,"value":3538},"g",{"type":23,"tag":135,"props":3540,"children":3541},{"style":163},[3542],{"type":28,"value":2917},{"type":23,"tag":135,"props":3544,"children":3545},{"style":184},[3546],{"type":28,"value":3547},"\" \"",{"type":23,"tag":135,"props":3549,"children":3550},{"style":163},[3551],{"type":28,"value":3552},"),\n",{"type":23,"tag":135,"props":3554,"children":3555},{"class":137,"line":217},[3556,3561,3566,3570,3574,3579,3584],{"type":23,"tag":135,"props":3557,"children":3558},{"style":163},[3559],{"type":28,"value":3560},"    state_topic: ",{"type":23,"tag":135,"props":3562,"children":3563},{"style":184},[3564],{"type":28,"value":3565},"`solar/${",{"type":23,"tag":135,"props":3567,"children":3568},{"style":163},[3569],{"type":28,"value":3455},{"type":23,"tag":135,"props":3571,"children":3572},{"style":184},[3573],{"type":28,"value":1783},{"type":23,"tag":135,"props":3575,"children":3576},{"style":163},[3577],{"type":28,"value":3578},"name",{"type":23,"tag":135,"props":3580,"children":3581},{"style":184},[3582],{"type":28,"value":3583},"}`",{"type":23,"tag":135,"props":3585,"children":3586},{"style":163},[3587],{"type":28,"value":192},{"type":23,"tag":135,"props":3589,"children":3590},{"class":137,"line":239},[3591],{"type":23,"tag":135,"props":3592,"children":3593},{"style":163},[3594],{"type":28,"value":3595},"    unit_of_measurement: reg.unit,\n",{"type":23,"tag":135,"props":3597,"children":3598},{"class":137,"line":261},[3599],{"type":23,"tag":135,"props":3600,"children":3601},{"style":163},[3602],{"type":28,"value":3603},"    device_class: reg.deviceClass,\n",{"type":23,"tag":135,"props":3605,"children":3606},{"class":137,"line":283},[3607],{"type":23,"tag":135,"props":3608,"children":3609},{"style":163},[3610],{"type":28,"value":3611},"    state_class: reg.stateClass,\n",{"type":23,"tag":135,"props":3613,"children":3614},{"class":137,"line":305},[3615,3620,3625,3629,3633,3637,3641],{"type":23,"tag":135,"props":3616,"children":3617},{"style":163},[3618],{"type":28,"value":3619},"    unique_id: ",{"type":23,"tag":135,"props":3621,"children":3622},{"style":184},[3623],{"type":28,"value":3624},"`sph5048_${",{"type":23,"tag":135,"props":3626,"children":3627},{"style":163},[3628],{"type":28,"value":3455},{"type":23,"tag":135,"props":3630,"children":3631},{"style":184},[3632],{"type":28,"value":1783},{"type":23,"tag":135,"props":3634,"children":3635},{"style":163},[3636],{"type":28,"value":3578},{"type":23,"tag":135,"props":3638,"children":3639},{"style":184},[3640],{"type":28,"value":3583},{"type":23,"tag":135,"props":3642,"children":3643},{"style":163},[3644],{"type":28,"value":192},{"type":23,"tag":135,"props":3646,"children":3647},{"class":137,"line":319},[3648],{"type":23,"tag":135,"props":3649,"children":3650},{"style":163},[3651],{"type":28,"value":3652},"    device: {\n",{"type":23,"tag":135,"props":3654,"children":3655},{"class":137,"line":343},[3656,3661,3665],{"type":23,"tag":135,"props":3657,"children":3658},{"style":163},[3659],{"type":28,"value":3660},"      identifiers: [",{"type":23,"tag":135,"props":3662,"children":3663},{"style":184},[3664],{"type":28,"value":335},{"type":23,"tag":135,"props":3666,"children":3667},{"style":163},[3668],{"type":28,"value":340},{"type":23,"tag":135,"props":3670,"children":3671},{"class":137,"line":361},[3672,3677,3682],{"type":23,"tag":135,"props":3673,"children":3674},{"style":163},[3675],{"type":28,"value":3676},"      name: ",{"type":23,"tag":135,"props":3678,"children":3679},{"style":184},[3680],{"type":28,"value":3681},"\"Solar Inverter\"",{"type":23,"tag":135,"props":3683,"children":3684},{"style":163},[3685],{"type":28,"value":192},{"type":23,"tag":135,"props":3687,"children":3688},{"class":137,"line":370},[3689,3694,3699],{"type":23,"tag":135,"props":3690,"children":3691},{"style":163},[3692],{"type":28,"value":3693},"      model: ",{"type":23,"tag":135,"props":3695,"children":3696},{"style":184},[3697],{"type":28,"value":3698},"\"SPH5048\"",{"type":23,"tag":135,"props":3700,"children":3701},{"style":163},[3702],{"type":28,"value":192},{"type":23,"tag":135,"props":3704,"children":3705},{"class":137,"line":1311},[3706,3711,3716],{"type":23,"tag":135,"props":3707,"children":3708},{"style":163},[3709],{"type":28,"value":3710},"      manufacturer: ",{"type":23,"tag":135,"props":3712,"children":3713},{"style":184},[3714],{"type":28,"value":3715},"\"Sungoldpower\"",{"type":23,"tag":135,"props":3717,"children":3718},{"style":163},[3719],{"type":28,"value":192},{"type":23,"tag":135,"props":3721,"children":3722},{"class":137,"line":1320},[3723],{"type":23,"tag":135,"props":3724,"children":3725},{"style":163},[3726],{"type":28,"value":3727},"    },\n",{"type":23,"tag":135,"props":3729,"children":3730},{"class":137,"line":1329},[3731],{"type":23,"tag":135,"props":3732,"children":3733},{"style":163},[3734],{"type":28,"value":3735},"  };\n",{"type":23,"tag":135,"props":3737,"children":3738},{"class":137,"line":1338},[3739],{"type":23,"tag":135,"props":3740,"children":3741},{"emptyLinePlaceholder":1226},[3742],{"type":28,"value":1229},{"type":23,"tag":135,"props":3744,"children":3745},{"class":137,"line":1347},[3746,3751,3756],{"type":23,"tag":135,"props":3747,"children":3748},{"style":163},[3749],{"type":28,"value":3750},"  mqttClient.",{"type":23,"tag":135,"props":3752,"children":3753},{"style":1990},[3754],{"type":28,"value":3755},"publish",{"type":23,"tag":135,"props":3757,"children":3758},{"style":163},[3759],{"type":28,"value":3760},"(\n",{"type":23,"tag":135,"props":3762,"children":3763},{"class":137,"line":1355},[3764,3769,3773,3777,3781,3786],{"type":23,"tag":135,"props":3765,"children":3766},{"style":184},[3767],{"type":28,"value":3768},"    `homeassistant/sensor/sph5048/${",{"type":23,"tag":135,"props":3770,"children":3771},{"style":163},[3772],{"type":28,"value":3455},{"type":23,"tag":135,"props":3774,"children":3775},{"style":184},[3776],{"type":28,"value":1783},{"type":23,"tag":135,"props":3778,"children":3779},{"style":163},[3780],{"type":28,"value":3578},{"type":23,"tag":135,"props":3782,"children":3783},{"style":184},[3784],{"type":28,"value":3785},"}/config`",{"type":23,"tag":135,"props":3787,"children":3788},{"style":163},[3789],{"type":28,"value":192},{"type":23,"tag":135,"props":3791,"children":3792},{"class":137,"line":1364},[3793,3798,3802,3807],{"type":23,"tag":135,"props":3794,"children":3795},{"style":173},[3796],{"type":28,"value":3797},"    JSON",{"type":23,"tag":135,"props":3799,"children":3800},{"style":163},[3801],{"type":28,"value":1783},{"type":23,"tag":135,"props":3803,"children":3804},{"style":1990},[3805],{"type":28,"value":3806},"stringify",{"type":23,"tag":135,"props":3808,"children":3809},{"style":163},[3810],{"type":28,"value":3811},"(config),\n",{"type":23,"tag":135,"props":3813,"children":3815},{"class":137,"line":3814},20,[3816,3821,3826],{"type":23,"tag":135,"props":3817,"children":3818},{"style":163},[3819],{"type":28,"value":3820},"    { retain: ",{"type":23,"tag":135,"props":3822,"children":3823},{"style":173},[3824],{"type":28,"value":3825},"true",{"type":23,"tag":135,"props":3827,"children":3828},{"style":163},[3829],{"type":28,"value":3830}," }\n",{"type":23,"tag":135,"props":3832,"children":3834},{"class":137,"line":3833},21,[3835],{"type":23,"tag":135,"props":3836,"children":3837},{"style":163},[3838],{"type":28,"value":3839},"  );\n",{"type":23,"tag":135,"props":3841,"children":3843},{"class":137,"line":3842},22,[3844],{"type":23,"tag":135,"props":3845,"children":3846},{"style":163},[3847],{"type":28,"value":376},{"type":23,"tag":24,"props":3849,"children":3850},{},[3851],{"type":28,"value":3852},"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":23,"tag":24,"props":3854,"children":3855},{},[3856,3858,3864],{"type":28,"value":3857},"Adding a new register to the ",{"type":23,"tag":131,"props":3859,"children":3861},{"className":3860},[],[3862],{"type":28,"value":3863},"REGISTERS",{"type":28,"value":3865}," 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":23,"tag":36,"props":3867,"children":3869},{"id":3868},"energy-tracking-with-trapezoidal-integration",[3870],{"type":28,"value":3871},"Energy Tracking With Trapezoidal Integration",{"type":23,"tag":24,"props":3873,"children":3874},{},[3875,3877,3883],{"type":28,"value":3876},"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":23,"tag":131,"props":3878,"children":3880},{"className":3879},[],[3881],{"type":28,"value":3882},"total_increasing",{"type":28,"value":3884}," sensors with kWh units.",{"type":23,"tag":24,"props":3886,"children":3887},{},[3888],{"type":28,"value":3889},"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":23,"tag":125,"props":3891,"children":3893},{"code":3892,"language":1806,"meta":8,"className":1900,"style":8},"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",[3894],{"type":23,"tag":131,"props":3895,"children":3896},{"__ignoreMap":8},[3897,3914,3939,3973,4005,4012,4042,4064,4088,4112,4119,4126,4184,4239,4287,4334,4379,4386,4406,4426,4443,4450,4457,4488,4504,4512],{"type":23,"tag":135,"props":3898,"children":3899},{"class":137,"line":138},[3900,3905,3910],{"type":23,"tag":135,"props":3901,"children":3902},{"style":820},[3903],{"type":28,"value":3904},"class",{"type":23,"tag":135,"props":3906,"children":3907},{"style":1990},[3908],{"type":28,"value":3909}," EnergyAccumulator",{"type":23,"tag":135,"props":3911,"children":3912},{"style":163},[3913],{"type":28,"value":2191},{"type":23,"tag":135,"props":3915,"children":3916},{"class":137,"line":169},[3917,3922,3927,3931,3935],{"type":23,"tag":135,"props":3918,"children":3919},{"style":820},[3920],{"type":28,"value":3921},"  private",{"type":23,"tag":135,"props":3923,"children":3924},{"style":2197},[3925],{"type":28,"value":3926}," totalKwh",{"type":23,"tag":135,"props":3928,"children":3929},{"style":820},[3930],{"type":28,"value":2205},{"type":23,"tag":135,"props":3932,"children":3933},{"style":173},[3934],{"type":28,"value":2210},{"type":23,"tag":135,"props":3936,"children":3937},{"style":163},[3938],{"type":28,"value":1932},{"type":23,"tag":135,"props":3940,"children":3941},{"class":137,"line":195},[3942,3946,3951,3955,3959,3964,3969],{"type":23,"tag":135,"props":3943,"children":3944},{"style":820},[3945],{"type":28,"value":3921},{"type":23,"tag":135,"props":3947,"children":3948},{"style":2197},[3949],{"type":28,"value":3950}," lastPowerW",{"type":23,"tag":135,"props":3952,"children":3953},{"style":820},[3954],{"type":28,"value":2205},{"type":23,"tag":135,"props":3956,"children":3957},{"style":173},[3958],{"type":28,"value":2210},{"type":23,"tag":135,"props":3960,"children":3961},{"style":820},[3962],{"type":28,"value":3963}," |",{"type":23,"tag":135,"props":3965,"children":3966},{"style":173},[3967],{"type":28,"value":3968}," null",{"type":23,"tag":135,"props":3970,"children":3971},{"style":163},[3972],{"type":28,"value":1932},{"type":23,"tag":135,"props":3974,"children":3975},{"class":137,"line":217},[3976,3980,3985,3989,3993,3997,4001],{"type":23,"tag":135,"props":3977,"children":3978},{"style":820},[3979],{"type":28,"value":3921},{"type":23,"tag":135,"props":3981,"children":3982},{"style":2197},[3983],{"type":28,"value":3984}," lastTs",{"type":23,"tag":135,"props":3986,"children":3987},{"style":820},[3988],{"type":28,"value":2205},{"type":23,"tag":135,"props":3990,"children":3991},{"style":173},[3992],{"type":28,"value":2210},{"type":23,"tag":135,"props":3994,"children":3995},{"style":820},[3996],{"type":28,"value":3963},{"type":23,"tag":135,"props":3998,"children":3999},{"style":173},[4000],{"type":28,"value":3968},{"type":23,"tag":135,"props":4002,"children":4003},{"style":163},[4004],{"type":28,"value":1932},{"type":23,"tag":135,"props":4006,"children":4007},{"class":137,"line":239},[4008],{"type":23,"tag":135,"props":4009,"children":4010},{"emptyLinePlaceholder":1226},[4011],{"type":28,"value":1229},{"type":23,"tag":135,"props":4013,"children":4014},{"class":137,"line":261},[4015,4020,4024,4029,4033,4038],{"type":23,"tag":135,"props":4016,"children":4017},{"style":820},[4018],{"type":28,"value":4019},"  constructor",{"type":23,"tag":135,"props":4021,"children":4022},{"style":163},[4023],{"type":28,"value":2021},{"type":23,"tag":135,"props":4025,"children":4026},{"style":2197},[4027],{"type":28,"value":4028},"initialKwh",{"type":23,"tag":135,"props":4030,"children":4031},{"style":820},[4032],{"type":28,"value":1982},{"type":23,"tag":135,"props":4034,"children":4035},{"style":173},[4036],{"type":28,"value":4037}," 0",{"type":23,"tag":135,"props":4039,"children":4040},{"style":163},[4041],{"type":28,"value":3147},{"type":23,"tag":135,"props":4043,"children":4044},{"class":137,"line":283},[4045,4050,4055,4059],{"type":23,"tag":135,"props":4046,"children":4047},{"style":173},[4048],{"type":28,"value":4049},"    this",{"type":23,"tag":135,"props":4051,"children":4052},{"style":163},[4053],{"type":28,"value":4054},".totalKwh ",{"type":23,"tag":135,"props":4056,"children":4057},{"style":820},[4058],{"type":28,"value":823},{"type":23,"tag":135,"props":4060,"children":4061},{"style":163},[4062],{"type":28,"value":4063}," initialKwh;\n",{"type":23,"tag":135,"props":4065,"children":4066},{"class":137,"line":305},[4067,4071,4076,4080,4084],{"type":23,"tag":135,"props":4068,"children":4069},{"style":173},[4070],{"type":28,"value":4049},{"type":23,"tag":135,"props":4072,"children":4073},{"style":163},[4074],{"type":28,"value":4075},".lastPowerW ",{"type":23,"tag":135,"props":4077,"children":4078},{"style":820},[4079],{"type":28,"value":823},{"type":23,"tag":135,"props":4081,"children":4082},{"style":173},[4083],{"type":28,"value":3968},{"type":23,"tag":135,"props":4085,"children":4086},{"style":163},[4087],{"type":28,"value":1932},{"type":23,"tag":135,"props":4089,"children":4090},{"class":137,"line":319},[4091,4095,4100,4104,4108],{"type":23,"tag":135,"props":4092,"children":4093},{"style":173},[4094],{"type":28,"value":4049},{"type":23,"tag":135,"props":4096,"children":4097},{"style":163},[4098],{"type":28,"value":4099},".lastTs ",{"type":23,"tag":135,"props":4101,"children":4102},{"style":820},[4103],{"type":28,"value":823},{"type":23,"tag":135,"props":4105,"children":4106},{"style":173},[4107],{"type":28,"value":3968},{"type":23,"tag":135,"props":4109,"children":4110},{"style":163},[4111],{"type":28,"value":1932},{"type":23,"tag":135,"props":4113,"children":4114},{"class":137,"line":343},[4115],{"type":23,"tag":135,"props":4116,"children":4117},{"style":163},[4118],{"type":28,"value":367},{"type":23,"tag":135,"props":4120,"children":4121},{"class":137,"line":361},[4122],{"type":23,"tag":135,"props":4123,"children":4124},{"emptyLinePlaceholder":1226},[4125],{"type":28,"value":1229},{"type":23,"tag":135,"props":4127,"children":4128},{"class":137,"line":370},[4129,4134,4138,4143,4147,4151,4155,4160,4164,4168,4172,4176,4180],{"type":23,"tag":135,"props":4130,"children":4131},{"style":1990},[4132],{"type":28,"value":4133},"  update",{"type":23,"tag":135,"props":4135,"children":4136},{"style":163},[4137],{"type":28,"value":2021},{"type":23,"tag":135,"props":4139,"children":4140},{"style":2197},[4141],{"type":28,"value":4142},"powerW",{"type":23,"tag":135,"props":4144,"children":4145},{"style":820},[4146],{"type":28,"value":2205},{"type":23,"tag":135,"props":4148,"children":4149},{"style":173},[4150],{"type":28,"value":2210},{"type":23,"tag":135,"props":4152,"children":4153},{"style":163},[4154],{"type":28,"value":2917},{"type":23,"tag":135,"props":4156,"children":4157},{"style":2197},[4158],{"type":28,"value":4159},"nowMs",{"type":23,"tag":135,"props":4161,"children":4162},{"style":820},[4163],{"type":28,"value":2205},{"type":23,"tag":135,"props":4165,"children":4166},{"style":173},[4167],{"type":28,"value":2210},{"type":23,"tag":135,"props":4169,"children":4170},{"style":163},[4171],{"type":28,"value":3468},{"type":23,"tag":135,"props":4173,"children":4174},{"style":820},[4175],{"type":28,"value":2205},{"type":23,"tag":135,"props":4177,"children":4178},{"style":173},[4179],{"type":28,"value":2210},{"type":23,"tag":135,"props":4181,"children":4182},{"style":163},[4183],{"type":28,"value":2191},{"type":23,"tag":135,"props":4185,"children":4186},{"class":137,"line":1311},[4187,4191,4195,4200,4204,4209,4213,4218,4223,4227,4231,4235],{"type":23,"tag":135,"props":4188,"children":4189},{"style":820},[4190],{"type":28,"value":3155},{"type":23,"tag":135,"props":4192,"children":4193},{"style":163},[4194],{"type":28,"value":3093},{"type":23,"tag":135,"props":4196,"children":4197},{"style":173},[4198],{"type":28,"value":4199},"this",{"type":23,"tag":135,"props":4201,"children":4202},{"style":163},[4203],{"type":28,"value":4075},{"type":23,"tag":135,"props":4205,"children":4206},{"style":820},[4207],{"type":28,"value":4208},"!==",{"type":23,"tag":135,"props":4210,"children":4211},{"style":173},[4212],{"type":28,"value":3968},{"type":23,"tag":135,"props":4214,"children":4215},{"style":820},[4216],{"type":28,"value":4217}," &&",{"type":23,"tag":135,"props":4219,"children":4220},{"style":173},[4221],{"type":28,"value":4222}," this",{"type":23,"tag":135,"props":4224,"children":4225},{"style":163},[4226],{"type":28,"value":4099},{"type":23,"tag":135,"props":4228,"children":4229},{"style":820},[4230],{"type":28,"value":4208},{"type":23,"tag":135,"props":4232,"children":4233},{"style":173},[4234],{"type":28,"value":3968},{"type":23,"tag":135,"props":4236,"children":4237},{"style":163},[4238],{"type":28,"value":3147},{"type":23,"tag":135,"props":4240,"children":4241},{"class":137,"line":1320},[4242,4247,4252,4256,4261,4265,4269,4274,4278,4283],{"type":23,"tag":135,"props":4243,"children":4244},{"style":820},[4245],{"type":28,"value":4246},"      const",{"type":23,"tag":135,"props":4248,"children":4249},{"style":173},[4250],{"type":28,"value":4251}," dtHours",{"type":23,"tag":135,"props":4253,"children":4254},{"style":820},[4255],{"type":28,"value":1982},{"type":23,"tag":135,"props":4257,"children":4258},{"style":163},[4259],{"type":28,"value":4260}," (nowMs ",{"type":23,"tag":135,"props":4262,"children":4263},{"style":820},[4264],{"type":28,"value":2942},{"type":23,"tag":135,"props":4266,"children":4267},{"style":173},[4268],{"type":28,"value":4222},{"type":23,"tag":135,"props":4270,"children":4271},{"style":163},[4272],{"type":28,"value":4273},".lastTs) ",{"type":23,"tag":135,"props":4275,"children":4276},{"style":820},[4277],{"type":28,"value":3523},{"type":23,"tag":135,"props":4279,"children":4280},{"style":173},[4281],{"type":28,"value":4282}," 3_600_000",{"type":23,"tag":135,"props":4284,"children":4285},{"style":163},[4286],{"type":28,"value":1932},{"type":23,"tag":135,"props":4288,"children":4289},{"class":137,"line":1329},[4290,4294,4299,4303,4308,4312,4316,4321,4325,4330],{"type":23,"tag":135,"props":4291,"children":4292},{"style":820},[4293],{"type":28,"value":4246},{"type":23,"tag":135,"props":4295,"children":4296},{"style":173},[4297],{"type":28,"value":4298}," avgW",{"type":23,"tag":135,"props":4300,"children":4301},{"style":820},[4302],{"type":28,"value":1982},{"type":23,"tag":135,"props":4304,"children":4305},{"style":163},[4306],{"type":28,"value":4307}," (powerW ",{"type":23,"tag":135,"props":4309,"children":4310},{"style":820},[4311],{"type":28,"value":3188},{"type":23,"tag":135,"props":4313,"children":4314},{"style":173},[4315],{"type":28,"value":4222},{"type":23,"tag":135,"props":4317,"children":4318},{"style":163},[4319],{"type":28,"value":4320},".lastPowerW) ",{"type":23,"tag":135,"props":4322,"children":4323},{"style":820},[4324],{"type":28,"value":3523},{"type":23,"tag":135,"props":4326,"children":4327},{"style":173},[4328],{"type":28,"value":4329}," 2",{"type":23,"tag":135,"props":4331,"children":4332},{"style":163},[4333],{"type":28,"value":1932},{"type":23,"tag":135,"props":4335,"children":4336},{"class":137,"line":1338},[4337,4342,4346,4351,4356,4361,4366,4370,4375],{"type":23,"tag":135,"props":4338,"children":4339},{"style":173},[4340],{"type":28,"value":4341},"      this",{"type":23,"tag":135,"props":4343,"children":4344},{"style":163},[4345],{"type":28,"value":4054},{"type":23,"tag":135,"props":4347,"children":4348},{"style":820},[4349],{"type":28,"value":4350},"+=",{"type":23,"tag":135,"props":4352,"children":4353},{"style":163},[4354],{"type":28,"value":4355}," (avgW ",{"type":23,"tag":135,"props":4357,"children":4358},{"style":820},[4359],{"type":28,"value":4360},"*",{"type":23,"tag":135,"props":4362,"children":4363},{"style":163},[4364],{"type":28,"value":4365}," dtHours) ",{"type":23,"tag":135,"props":4367,"children":4368},{"style":820},[4369],{"type":28,"value":3523},{"type":23,"tag":135,"props":4371,"children":4372},{"style":173},[4373],{"type":28,"value":4374}," 1000",{"type":23,"tag":135,"props":4376,"children":4377},{"style":163},[4378],{"type":28,"value":1932},{"type":23,"tag":135,"props":4380,"children":4381},{"class":137,"line":1347},[4382],{"type":23,"tag":135,"props":4383,"children":4384},{"style":163},[4385],{"type":28,"value":3291},{"type":23,"tag":135,"props":4387,"children":4388},{"class":137,"line":1355},[4389,4393,4397,4401],{"type":23,"tag":135,"props":4390,"children":4391},{"style":173},[4392],{"type":28,"value":4049},{"type":23,"tag":135,"props":4394,"children":4395},{"style":163},[4396],{"type":28,"value":4075},{"type":23,"tag":135,"props":4398,"children":4399},{"style":820},[4400],{"type":28,"value":823},{"type":23,"tag":135,"props":4402,"children":4403},{"style":163},[4404],{"type":28,"value":4405}," powerW;\n",{"type":23,"tag":135,"props":4407,"children":4408},{"class":137,"line":1364},[4409,4413,4417,4421],{"type":23,"tag":135,"props":4410,"children":4411},{"style":173},[4412],{"type":28,"value":4049},{"type":23,"tag":135,"props":4414,"children":4415},{"style":163},[4416],{"type":28,"value":4099},{"type":23,"tag":135,"props":4418,"children":4419},{"style":820},[4420],{"type":28,"value":823},{"type":23,"tag":135,"props":4422,"children":4423},{"style":163},[4424],{"type":28,"value":4425}," nowMs;\n",{"type":23,"tag":135,"props":4427,"children":4428},{"class":137,"line":3814},[4429,4434,4438],{"type":23,"tag":135,"props":4430,"children":4431},{"style":820},[4432],{"type":28,"value":4433},"    return",{"type":23,"tag":135,"props":4435,"children":4436},{"style":173},[4437],{"type":28,"value":4222},{"type":23,"tag":135,"props":4439,"children":4440},{"style":163},[4441],{"type":28,"value":4442},".totalKwh;\n",{"type":23,"tag":135,"props":4444,"children":4445},{"class":137,"line":3833},[4446],{"type":23,"tag":135,"props":4447,"children":4448},{"style":163},[4449],{"type":28,"value":367},{"type":23,"tag":135,"props":4451,"children":4452},{"class":137,"line":3842},[4453],{"type":23,"tag":135,"props":4454,"children":4455},{"emptyLinePlaceholder":1226},[4456],{"type":28,"value":1229},{"type":23,"tag":135,"props":4458,"children":4460},{"class":137,"line":4459},23,[4461,4466,4471,4476,4480,4484],{"type":23,"tag":135,"props":4462,"children":4463},{"style":820},[4464],{"type":28,"value":4465},"  get",{"type":23,"tag":135,"props":4467,"children":4468},{"style":1990},[4469],{"type":28,"value":4470}," value",{"type":23,"tag":135,"props":4472,"children":4473},{"style":163},[4474],{"type":28,"value":4475},"()",{"type":23,"tag":135,"props":4477,"children":4478},{"style":820},[4479],{"type":28,"value":2205},{"type":23,"tag":135,"props":4481,"children":4482},{"style":173},[4483],{"type":28,"value":2210},{"type":23,"tag":135,"props":4485,"children":4486},{"style":163},[4487],{"type":28,"value":2191},{"type":23,"tag":135,"props":4489,"children":4491},{"class":137,"line":4490},24,[4492,4496,4500],{"type":23,"tag":135,"props":4493,"children":4494},{"style":820},[4495],{"type":28,"value":4433},{"type":23,"tag":135,"props":4497,"children":4498},{"style":173},[4499],{"type":28,"value":4222},{"type":23,"tag":135,"props":4501,"children":4502},{"style":163},[4503],{"type":28,"value":4442},{"type":23,"tag":135,"props":4505,"children":4507},{"class":137,"line":4506},25,[4508],{"type":23,"tag":135,"props":4509,"children":4510},{"style":163},[4511],{"type":28,"value":367},{"type":23,"tag":135,"props":4513,"children":4515},{"class":137,"line":4514},26,[4516],{"type":23,"tag":135,"props":4517,"children":4518},{"style":163},[4519],{"type":28,"value":376},{"type":23,"tag":24,"props":4521,"children":4522},{},[4523],{"type":28,"value":4524},"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":23,"tag":24,"props":4526,"children":4527},{},[4528,4530,4535],{"type":28,"value":4529},"These publish as ",{"type":23,"tag":131,"props":4531,"children":4533},{"className":4532},[],[4534],{"type":28,"value":3882},{"type":28,"value":4536}," 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":23,"tag":36,"props":4538,"children":4540},{"id":4539},"timescaledb-continuous-aggregates-and-retention",[4541],{"type":28,"value":4542},"TimescaleDB: Continuous Aggregates and Retention",{"type":23,"tag":24,"props":4544,"children":4545},{},[4546,4548,4553],{"type":28,"value":4547},"The schema is the same basic approach as before, ",{"type":23,"tag":131,"props":4549,"children":4551},{"className":4550},[],[4552],{"type":28,"value":1652},{"type":28,"value":4554}," rows in a hypertable. The difference is continuous aggregates.",{"type":23,"tag":24,"props":4556,"children":4557},{},[4558],{"type":28,"value":4559},"TimescaleDB can maintain pre-computed rollups that stay current automatically:",{"type":23,"tag":125,"props":4561,"children":4563},{"code":4562,"language":582,"meta":8,"className":580,"style":8},"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",[4564],{"type":23,"tag":131,"props":4565,"children":4566},{"__ignoreMap":8},[4567,4575,4583,4591,4599,4607,4615,4623,4631,4638,4646,4653,4661,4669,4677,4685],{"type":23,"tag":135,"props":4568,"children":4569},{"class":137,"line":138},[4570],{"type":23,"tag":135,"props":4571,"children":4572},{},[4573],{"type":28,"value":4574},"CREATE MATERIALIZED VIEW inverter_hourly\n",{"type":23,"tag":135,"props":4576,"children":4577},{"class":137,"line":169},[4578],{"type":23,"tag":135,"props":4579,"children":4580},{},[4581],{"type":28,"value":4582},"WITH (timescaledb.continuous) AS\n",{"type":23,"tag":135,"props":4584,"children":4585},{"class":137,"line":195},[4586],{"type":23,"tag":135,"props":4587,"children":4588},{},[4589],{"type":28,"value":4590},"SELECT\n",{"type":23,"tag":135,"props":4592,"children":4593},{"class":137,"line":217},[4594],{"type":23,"tag":135,"props":4595,"children":4596},{},[4597],{"type":28,"value":4598},"  time_bucket('1 hour', ts) AS bucket,\n",{"type":23,"tag":135,"props":4600,"children":4601},{"class":137,"line":239},[4602],{"type":23,"tag":135,"props":4603,"children":4604},{},[4605],{"type":28,"value":4606},"  key,\n",{"type":23,"tag":135,"props":4608,"children":4609},{"class":137,"line":261},[4610],{"type":23,"tag":135,"props":4611,"children":4612},{},[4613],{"type":28,"value":4614},"  avg(value)  AS avg_val,\n",{"type":23,"tag":135,"props":4616,"children":4617},{"class":137,"line":283},[4618],{"type":23,"tag":135,"props":4619,"children":4620},{},[4621],{"type":28,"value":4622},"  min(value)  AS min_val,\n",{"type":23,"tag":135,"props":4624,"children":4625},{"class":137,"line":305},[4626],{"type":23,"tag":135,"props":4627,"children":4628},{},[4629],{"type":28,"value":4630},"  max(value)  AS max_val\n",{"type":23,"tag":135,"props":4632,"children":4633},{"class":137,"line":319},[4634],{"type":23,"tag":135,"props":4635,"children":4636},{},[4637],{"type":28,"value":610},{"type":23,"tag":135,"props":4639,"children":4640},{"class":137,"line":343},[4641],{"type":23,"tag":135,"props":4642,"children":4643},{},[4644],{"type":28,"value":4645},"GROUP BY bucket, key;\n",{"type":23,"tag":135,"props":4647,"children":4648},{"class":137,"line":361},[4649],{"type":23,"tag":135,"props":4650,"children":4651},{"emptyLinePlaceholder":1226},[4652],{"type":28,"value":1229},{"type":23,"tag":135,"props":4654,"children":4655},{"class":137,"line":370},[4656],{"type":23,"tag":135,"props":4657,"children":4658},{},[4659],{"type":28,"value":4660},"SELECT add_continuous_aggregate_policy('inverter_hourly',\n",{"type":23,"tag":135,"props":4662,"children":4663},{"class":137,"line":1311},[4664],{"type":23,"tag":135,"props":4665,"children":4666},{},[4667],{"type":28,"value":4668},"  start_offset => INTERVAL '3 hours',\n",{"type":23,"tag":135,"props":4670,"children":4671},{"class":137,"line":1320},[4672],{"type":23,"tag":135,"props":4673,"children":4674},{},[4675],{"type":28,"value":4676},"  end_offset   => INTERVAL '1 hour',\n",{"type":23,"tag":135,"props":4678,"children":4679},{"class":137,"line":1329},[4680],{"type":23,"tag":135,"props":4681,"children":4682},{},[4683],{"type":28,"value":4684},"  schedule_interval => INTERVAL '1 hour'\n",{"type":23,"tag":135,"props":4686,"children":4687},{"class":137,"line":1338},[4688],{"type":23,"tag":135,"props":4689,"children":4690},{},[4691],{"type":28,"value":1611},{"type":23,"tag":24,"props":4693,"children":4694},{},[4695,4697,4703],{"type":28,"value":4696},"Daily aggregates use ",{"type":23,"tag":131,"props":4698,"children":4700},{"className":4699},[],[4701],{"type":28,"value":4702},"time_bucket_gapfill",{"type":28,"value":4704}," 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":23,"tag":24,"props":4706,"children":4707},{},[4708],{"type":28,"value":4709},"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":23,"tag":36,"props":4711,"children":4713},{"id":4712},"the-full-stack-in-docker-compose",[4714],{"type":28,"value":4715},"The Full Stack in Docker Compose",{"type":23,"tag":24,"props":4717,"children":4718},{},[4719],{"type":28,"value":4720},"Everything runs in a single Compose file:",{"type":23,"tag":125,"props":4722,"children":4724},{"code":4723,"language":454,"meta":8,"className":452,"style":8},"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",[4725],{"type":23,"tag":131,"props":4726,"children":4727},{"__ignoreMap":8},[4728,4739,4751,4767,4779,4792,4804,4816,4828,4835,4846,4861,4872,4887,4902,4917,4928,4940,4952,4959,4971,4988,5000,5012,5024,5035,5051,5068,5085,5101,5114,5127,5145,5153,5166,5179],{"type":23,"tag":135,"props":4729,"children":4730},{"class":137,"line":138},[4731,4735],{"type":23,"tag":135,"props":4732,"children":4733},{"style":464},[4734],{"type":28,"value":467},{"type":23,"tag":135,"props":4736,"children":4737},{"style":163},[4738],{"type":28,"value":472},{"type":23,"tag":135,"props":4740,"children":4741},{"class":137,"line":169},[4742,4747],{"type":23,"tag":135,"props":4743,"children":4744},{"style":464},[4745],{"type":28,"value":4746},"  mosquitto",{"type":23,"tag":135,"props":4748,"children":4749},{"style":163},[4750],{"type":28,"value":472},{"type":23,"tag":135,"props":4752,"children":4753},{"class":137,"line":195},[4754,4758,4762],{"type":23,"tag":135,"props":4755,"children":4756},{"style":464},[4757],{"type":28,"value":492},{"type":23,"tag":135,"props":4759,"children":4760},{"style":163},[4761],{"type":28,"value":181},{"type":23,"tag":135,"props":4763,"children":4764},{"style":184},[4765],{"type":28,"value":4766},"eclipse-mosquitto:2\n",{"type":23,"tag":135,"props":4768,"children":4769},{"class":137,"line":217},[4770,4775],{"type":23,"tag":135,"props":4771,"children":4772},{"style":464},[4773],{"type":28,"value":4774},"    volumes",{"type":23,"tag":135,"props":4776,"children":4777},{"style":163},[4778],{"type":28,"value":472},{"type":23,"tag":135,"props":4780,"children":4781},{"class":137,"line":239},[4782,4787],{"type":23,"tag":135,"props":4783,"children":4784},{"style":163},[4785],{"type":28,"value":4786},"      - ",{"type":23,"tag":135,"props":4788,"children":4789},{"style":184},[4790],{"type":28,"value":4791},"./mosquitto/config:/mosquitto/config\n",{"type":23,"tag":135,"props":4793,"children":4794},{"class":137,"line":261},[4795,4799],{"type":23,"tag":135,"props":4796,"children":4797},{"style":163},[4798],{"type":28,"value":4786},{"type":23,"tag":135,"props":4800,"children":4801},{"style":184},[4802],{"type":28,"value":4803},"mosquitto-data:/mosquitto/data\n",{"type":23,"tag":135,"props":4805,"children":4806},{"class":137,"line":283},[4807,4812],{"type":23,"tag":135,"props":4808,"children":4809},{"style":464},[4810],{"type":28,"value":4811},"    ports",{"type":23,"tag":135,"props":4813,"children":4814},{"style":163},[4815],{"type":28,"value":472},{"type":23,"tag":135,"props":4817,"children":4818},{"class":137,"line":305},[4819,4823],{"type":23,"tag":135,"props":4820,"children":4821},{"style":163},[4822],{"type":28,"value":4786},{"type":23,"tag":135,"props":4824,"children":4825},{"style":184},[4826],{"type":28,"value":4827},"\"1883:1883\"\n",{"type":23,"tag":135,"props":4829,"children":4830},{"class":137,"line":319},[4831],{"type":23,"tag":135,"props":4832,"children":4833},{"emptyLinePlaceholder":1226},[4834],{"type":28,"value":1229},{"type":23,"tag":135,"props":4836,"children":4837},{"class":137,"line":343},[4838,4842],{"type":23,"tag":135,"props":4839,"children":4840},{"style":464},[4841],{"type":28,"value":696},{"type":23,"tag":135,"props":4843,"children":4844},{"style":163},[4845],{"type":28,"value":472},{"type":23,"tag":135,"props":4847,"children":4848},{"class":137,"line":361},[4849,4853,4857],{"type":23,"tag":135,"props":4850,"children":4851},{"style":464},[4852],{"type":28,"value":492},{"type":23,"tag":135,"props":4854,"children":4855},{"style":163},[4856],{"type":28,"value":181},{"type":23,"tag":135,"props":4858,"children":4859},{"style":184},[4860],{"type":28,"value":716},{"type":23,"tag":135,"props":4862,"children":4863},{"class":137,"line":370},[4864,4868],{"type":23,"tag":135,"props":4865,"children":4866},{"style":464},[4867],{"type":28,"value":509},{"type":23,"tag":135,"props":4869,"children":4870},{"style":163},[4871],{"type":28,"value":472},{"type":23,"tag":135,"props":4873,"children":4874},{"class":137,"line":1311},[4875,4879,4883],{"type":23,"tag":135,"props":4876,"children":4877},{"style":464},[4878],{"type":28,"value":751},{"type":23,"tag":135,"props":4880,"children":4881},{"style":163},[4882],{"type":28,"value":181},{"type":23,"tag":135,"props":4884,"children":4885},{"style":184},[4886],{"type":28,"value":760},{"type":23,"tag":135,"props":4888,"children":4889},{"class":137,"line":1320},[4890,4894,4898],{"type":23,"tag":135,"props":4891,"children":4892},{"style":464},[4893],{"type":28,"value":768},{"type":23,"tag":135,"props":4895,"children":4896},{"style":163},[4897],{"type":28,"value":181},{"type":23,"tag":135,"props":4899,"children":4900},{"style":184},[4901],{"type":28,"value":760},{"type":23,"tag":135,"props":4903,"children":4904},{"class":137,"line":1329},[4905,4909,4913],{"type":23,"tag":135,"props":4906,"children":4907},{"style":464},[4908],{"type":28,"value":784},{"type":23,"tag":135,"props":4910,"children":4911},{"style":163},[4912],{"type":28,"value":181},{"type":23,"tag":135,"props":4914,"children":4915},{"style":184},[4916],{"type":28,"value":793},{"type":23,"tag":135,"props":4918,"children":4919},{"class":137,"line":1338},[4920,4924],{"type":23,"tag":135,"props":4921,"children":4922},{"style":464},[4923],{"type":28,"value":4774},{"type":23,"tag":135,"props":4925,"children":4926},{"style":163},[4927],{"type":28,"value":472},{"type":23,"tag":135,"props":4929,"children":4930},{"class":137,"line":1347},[4931,4935],{"type":23,"tag":135,"props":4932,"children":4933},{"style":163},[4934],{"type":28,"value":4786},{"type":23,"tag":135,"props":4936,"children":4937},{"style":184},[4938],{"type":28,"value":4939},"tsdb-data:/var/lib/postgresql/data\n",{"type":23,"tag":135,"props":4941,"children":4942},{"class":137,"line":1355},[4943,4947],{"type":23,"tag":135,"props":4944,"children":4945},{"style":163},[4946],{"type":28,"value":4786},{"type":23,"tag":135,"props":4948,"children":4949},{"style":184},[4950],{"type":28,"value":4951},"./sql/init.sql:/docker-entrypoint-initdb.d/init.sql\n",{"type":23,"tag":135,"props":4953,"children":4954},{"class":137,"line":1364},[4955],{"type":23,"tag":135,"props":4956,"children":4957},{"emptyLinePlaceholder":1226},[4958],{"type":28,"value":1229},{"type":23,"tag":135,"props":4960,"children":4961},{"class":137,"line":3814},[4962,4967],{"type":23,"tag":135,"props":4963,"children":4964},{"style":464},[4965],{"type":28,"value":4966},"  poller",{"type":23,"tag":135,"props":4968,"children":4969},{"style":163},[4970],{"type":28,"value":472},{"type":23,"tag":135,"props":4972,"children":4973},{"class":137,"line":3833},[4974,4979,4983],{"type":23,"tag":135,"props":4975,"children":4976},{"style":464},[4977],{"type":28,"value":4978},"    build",{"type":23,"tag":135,"props":4980,"children":4981},{"style":163},[4982],{"type":28,"value":181},{"type":23,"tag":135,"props":4984,"children":4985},{"style":184},[4986],{"type":28,"value":4987},"./poller\n",{"type":23,"tag":135,"props":4989,"children":4990},{"class":137,"line":3842},[4991,4996],{"type":23,"tag":135,"props":4992,"children":4993},{"style":464},[4994],{"type":28,"value":4995},"    depends_on",{"type":23,"tag":135,"props":4997,"children":4998},{"style":163},[4999],{"type":28,"value":472},{"type":23,"tag":135,"props":5001,"children":5002},{"class":137,"line":4459},[5003,5007],{"type":23,"tag":135,"props":5004,"children":5005},{"style":163},[5006],{"type":28,"value":4786},{"type":23,"tag":135,"props":5008,"children":5009},{"style":184},[5010],{"type":28,"value":5011},"mosquitto\n",{"type":23,"tag":135,"props":5013,"children":5014},{"class":137,"line":4490},[5015,5019],{"type":23,"tag":135,"props":5016,"children":5017},{"style":163},[5018],{"type":28,"value":4786},{"type":23,"tag":135,"props":5020,"children":5021},{"style":184},[5022],{"type":28,"value":5023},"timescaledb\n",{"type":23,"tag":135,"props":5025,"children":5026},{"class":137,"line":4506},[5027,5031],{"type":23,"tag":135,"props":5028,"children":5029},{"style":464},[5030],{"type":28,"value":509},{"type":23,"tag":135,"props":5032,"children":5033},{"style":163},[5034],{"type":28,"value":472},{"type":23,"tag":135,"props":5036,"children":5037},{"class":137,"line":4514},[5038,5043,5047],{"type":23,"tag":135,"props":5039,"children":5040},{"style":464},[5041],{"type":28,"value":5042},"      MQTT_HOST",{"type":23,"tag":135,"props":5044,"children":5045},{"style":163},[5046],{"type":28,"value":181},{"type":23,"tag":135,"props":5048,"children":5049},{"style":184},[5050],{"type":28,"value":5011},{"type":23,"tag":135,"props":5052,"children":5054},{"class":137,"line":5053},27,[5055,5060,5064],{"type":23,"tag":135,"props":5056,"children":5057},{"style":464},[5058],{"type":28,"value":5059},"      DB_HOST",{"type":23,"tag":135,"props":5061,"children":5062},{"style":163},[5063],{"type":28,"value":181},{"type":23,"tag":135,"props":5065,"children":5066},{"style":184},[5067],{"type":28,"value":5023},{"type":23,"tag":135,"props":5069,"children":5071},{"class":137,"line":5070},28,[5072,5077,5081],{"type":23,"tag":135,"props":5073,"children":5074},{"style":464},[5075],{"type":28,"value":5076},"      DB_PASSWORD",{"type":23,"tag":135,"props":5078,"children":5079},{"style":163},[5080],{"type":28,"value":181},{"type":23,"tag":135,"props":5082,"children":5083},{"style":184},[5084],{"type":28,"value":793},{"type":23,"tag":135,"props":5086,"children":5088},{"class":137,"line":5087},29,[5089,5093,5097],{"type":23,"tag":135,"props":5090,"children":5091},{"style":464},[5092],{"type":28,"value":521},{"type":23,"tag":135,"props":5094,"children":5095},{"style":163},[5096],{"type":28,"value":181},{"type":23,"tag":135,"props":5098,"children":5099},{"style":184},[5100],{"type":28,"value":743},{"type":23,"tag":135,"props":5102,"children":5104},{"class":137,"line":5103},30,[5105,5110],{"type":23,"tag":135,"props":5106,"children":5107},{"style":464},[5108],{"type":28,"value":5109},"    devices",{"type":23,"tag":135,"props":5111,"children":5112},{"style":163},[5113],{"type":28,"value":472},{"type":23,"tag":135,"props":5115,"children":5117},{"class":137,"line":5116},31,[5118,5122],{"type":23,"tag":135,"props":5119,"children":5120},{"style":163},[5121],{"type":28,"value":4786},{"type":23,"tag":135,"props":5123,"children":5124},{"style":184},[5125],{"type":28,"value":5126},"/dev/ttyUSB0:/dev/ttyUSB0\n",{"type":23,"tag":135,"props":5128,"children":5130},{"class":137,"line":5129},32,[5131,5136,5140],{"type":23,"tag":135,"props":5132,"children":5133},{"style":464},[5134],{"type":28,"value":5135},"    restart",{"type":23,"tag":135,"props":5137,"children":5138},{"style":163},[5139],{"type":28,"value":181},{"type":23,"tag":135,"props":5141,"children":5142},{"style":184},[5143],{"type":28,"value":5144},"unless-stopped\n",{"type":23,"tag":135,"props":5146,"children":5148},{"class":137,"line":5147},33,[5149],{"type":23,"tag":135,"props":5150,"children":5151},{"emptyLinePlaceholder":1226},[5152],{"type":28,"value":1229},{"type":23,"tag":135,"props":5154,"children":5156},{"class":137,"line":5155},34,[5157,5162],{"type":23,"tag":135,"props":5158,"children":5159},{"style":464},[5160],{"type":28,"value":5161},"volumes",{"type":23,"tag":135,"props":5163,"children":5164},{"style":163},[5165],{"type":28,"value":472},{"type":23,"tag":135,"props":5167,"children":5169},{"class":137,"line":5168},35,[5170,5175],{"type":23,"tag":135,"props":5171,"children":5172},{"style":464},[5173],{"type":28,"value":5174},"  mosquitto-data",{"type":23,"tag":135,"props":5176,"children":5177},{"style":163},[5178],{"type":28,"value":472},{"type":23,"tag":135,"props":5180,"children":5182},{"class":137,"line":5181},36,[5183,5188],{"type":23,"tag":135,"props":5184,"children":5185},{"style":464},[5186],{"type":28,"value":5187},"  tsdb-data",{"type":23,"tag":135,"props":5189,"children":5190},{"style":163},[5191],{"type":28,"value":472},{"type":23,"tag":24,"props":5193,"children":5194},{},[5195,5197,5202],{"type":28,"value":5196},"A ",{"type":23,"tag":131,"props":5198,"children":5200},{"className":5199},[],[5201],{"type":28,"value":644},{"type":28,"value":5203}," file carries the timezone and any secrets. The whole stack rebuilds from scratch in under a minute on a fresh machine.",{"type":23,"tag":24,"props":5205,"children":5206},{},[5207,5209,5214],{"type":28,"value":5208},"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":23,"tag":131,"props":5210,"children":5212},{"className":5211},[],[5213],{"type":28,"value":392},{"type":28,"value":5215},". The poller has no idea who's listening.",{"type":23,"tag":36,"props":5217,"children":5219},{"id":5218},"what-got-better",[5220],{"type":28,"value":5221},"What Got Better",{"type":23,"tag":24,"props":5223,"children":5224},{},[5225,5230],{"type":23,"tag":1392,"props":5226,"children":5227},{},[5228],{"type":28,"value":5229},"No more custom dashboard.",{"type":28,"value":5231}," 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":23,"tag":24,"props":5233,"children":5234},{},[5235,5240,5242,5247],{"type":23,"tag":1392,"props":5236,"children":5237},{},[5238],{"type":28,"value":5239},"Adding a metric takes three lines.",{"type":28,"value":5241}," Drop a new entry in the ",{"type":23,"tag":131,"props":5243,"children":5245},{"className":5244},[],[5246],{"type":28,"value":3863},{"type":28,"value":5248}," array with the address, name, scale, and unit. TypeScript compilation catches mistakes. The sensor appears in Home Assistant on the next deploy.",{"type":23,"tag":24,"props":5250,"children":5251},{},[5252,5257,5259,5265],{"type":23,"tag":1392,"props":5253,"children":5254},{},[5255],{"type":28,"value":5256},"The whole stack fits in a repository.",{"type":28,"value":5258}," ",{"type":23,"tag":131,"props":5260,"children":5262},{"className":5261},[],[5263],{"type":28,"value":5264},"docker compose up -d",{"type":28,"value":5266}," on a new machine and everything is back. That lesson cost me twice before I learned it.",{"type":23,"tag":24,"props":5268,"children":5269},{},[5270,5275],{"type":23,"tag":1392,"props":5271,"children":5272},{},[5273],{"type":28,"value":5274},"Energy tracking works.",{"type":28,"value":5276}," The trapezoidal accumulators and TimescaleDB continuous aggregates give me accurate daily kWh numbers without writing any aggregation code in the application layer.",{"type":23,"tag":36,"props":5278,"children":5280},{"id":5279},"what-i-learned",[5281],{"type":28,"value":5282},"What I Learned",{"type":23,"tag":24,"props":5284,"children":5285},{},[5286],{"type":28,"value":5287},"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":23,"tag":24,"props":5289,"children":5290},{},[5291],{"type":28,"value":5292},"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":23,"tag":24,"props":5294,"children":5295},{},[5296],{"type":28,"value":5297},"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":23,"tag":24,"props":5299,"children":5300},{},[5301],{"type":28,"value":5302},"And the thing that should have been obvious: push your code to a repository. Not someday. Before you deploy it.",{"type":23,"tag":1070,"props":5304,"children":5305},{},[],{"type":23,"tag":24,"props":5307,"children":5308},{},[5309],{"type":28,"value":5310},"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":23,"tag":24,"props":5312,"children":5313},{},[5314],{"type":28,"value":5315},"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":23,"tag":1027,"props":5317,"children":5318},{},[5319],{"type":28,"value":1031},{"title":8,"searchDepth":169,"depth":169,"links":5321},[5322,5323,5326,5327,5328,5329,5330,5331,5332],{"id":1857,"depth":169,"text":1860},{"id":1873,"depth":169,"text":1876,"children":5324},[5325],{"id":2749,"depth":195,"text":2752},{"id":3358,"depth":169,"text":3361},{"id":3387,"depth":169,"text":3390},{"id":3868,"depth":169,"text":3871},{"id":4539,"depth":169,"text":4542},{"id":4712,"depth":169,"text":4715},{"id":5218,"depth":169,"text":5221},{"id":5279,"depth":169,"text":5282},"content:blog:solar-monitoring-part-2-the-typescript-rebuild.md","blog/solar-monitoring-part-2-the-typescript-rebuild.md","blog/solar-monitoring-part-2-the-typescript-rebuild",{"_path":50,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":5337,"description":5338,"date":5339,"updated":5339,"tags":5340,"readingTime":5344,"cover":8,"body":5345,"_type":1041,"_id":5557,"_source":1043,"_file":5558,"_stem":5559,"_extension":1046},"Well, I Embarrassed Myself Even Sooner Than Expected: A Modular PSU Cables Tale","I fried a SATA SSD and HDD mixing Corsair cables on an EVGA PSU. The AI warned me. Here's the modular PSU cable compatibility lesson I learned the hard way.","2026-03-07",[15,5341,5342,5343],"hardware","ai-tools","lessons-learned","5 min read",{"type":20,"children":5346,"toc":5549},[5347,5352,5358,5363,5368,5373,5378,5384,5389,5394,5399,5404,5409,5415,5420,5425,5430,5435,5446,5451,5457,5462,5472,5477,5482,5488,5493,5498,5503,5508,5513,5519,5524,5529,5534,5539,5544],{"type":23,"tag":24,"props":5348,"children":5349},{},[5350],{"type":28,"value":5351},"I said from the start that this blog was about accountability. Turns out accountability doesn't wait around. I fried a SATA SSD and a hard drive within the first week of this new chapter of my homelab journey. The AI warned me. I didn't fully listen. Here's what happened.",{"type":23,"tag":36,"props":5353,"children":5355},{"id":5354},"how-we-got-here",[5356],{"type":28,"value":5357},"How We Got Here",{"type":23,"tag":24,"props":5359,"children":5360},{},[5361],{"type":28,"value":5362},"A bit of context. I have a 2014 HTPC that has lived many lives. Former home theater box, Linux server, and most recently, a dust collector. When AI started re-igniting my interest in tinkering (more on that dynamic in a future post), this old machine was the first thing I dusted off.",{"type":23,"tag":24,"props":5364,"children":5365},{},[5366],{"type":28,"value":5367},"I threw Ubuntu 24 on it, got my solar inverter polling, stood up a web interface, and even launched a crypto mining container. On an AMD A4 7300. I'm pulling in a blistering $0.007 per day. Early retirement is basically locked in.",{"type":23,"tag":24,"props":5369,"children":5370},{},[5371],{"type":28,"value":5372},"I also got Ollama running with a qwen 0.5b model. My local LLM could confirm that my solar array exists, read back three numbers, and present them in a completely wrong context. The future is now.",{"type":23,"tag":24,"props":5374,"children":5375},{},[5376],{"type":28,"value":5377},"The point is, I was having fun again. And that mattered.",{"type":23,"tag":36,"props":5379,"children":5381},{"id":5380},"the-upgrade-plan",[5382],{"type":28,"value":5383},"The Upgrade Plan",{"type":23,"tag":24,"props":5385,"children":5386},{},[5387],{"type":28,"value":5388},"My main PC had been freezing intermittently, and I wanted to do some upgrades. The plan was simple: buy new stuff for the main rig, and hand the hand-me-downs down to the 2014 server so it could actually run a local LLM worth talking to.",{"type":23,"tag":24,"props":5390,"children":5391},{},[5392],{"type":28,"value":5393},"Step one was dropping a used RTX 3050 8GB into the old Gigabyte board. Dual core AMD, 8 GB of RAM, 2014 vintage. The GPU physically fit after I swapped to a non-slim case, so I powered it on.",{"type":23,"tag":24,"props":5395,"children":5396},{},[5397],{"type":28,"value":5398},"The network card did not come up.",{"type":23,"tag":24,"props":5400,"children":5401},{},[5402],{"type":28,"value":5403},"What followed was a longer troubleshooting session than I'm going to admit to publicly. I went down a pretty deep rabbit hole with the help of AI trying to figure out what was wrong, and at some point the AI landed on a theory: the cheap PSU in that box couldn't handle the load. Voltage rail collapse, it said. Confidently. Repeatedly.",{"type":23,"tag":24,"props":5405,"children":5406},{},[5407],{"type":28,"value":5408},"I'm not saying it was wrong. I'm saying I'll never know, because I swapped the PSU before I fully ruled anything else out.",{"type":23,"tag":36,"props":5410,"children":5412},{"id":5411},"where-ai-was-right-and-where-i-ignored-it",[5413],{"type":28,"value":5414},"Where AI Was Right (And Where I Ignored It)",{"type":23,"tag":24,"props":5416,"children":5417},{},[5418],{"type":28,"value":5419},"This is the part that stings a little.",{"type":23,"tag":24,"props":5421,"children":5422},{},[5423],{"type":28,"value":5424},"When I told the AI I was swapping in a different PSU, it gave me a clear warning: do not mix modular PSU cables between brands. Just that. A warning without the full explanation of why. I didn't think to ask for clarification, and that's really where this went sideways. If I had pushed back and said \"wait, explain why SATA would matter,\" it almost certainly would have walked me through the whole proprietary pinout issue and I'd have known the stakes. I didn't ask. I just sort of nodded at the warning and moved on.",{"type":23,"tag":24,"props":5426,"children":5427},{},[5428],{"type":28,"value":5429},"Some context on the cable situation: I had just bought a new Corsair PSU for my main rig, which came with a fresh set of Corsair cables. The EVGA PSU was the one being handed down to the server. So I had new Corsair cables sitting right there, and the EVGA PSU was going into the build. I followed the AI's guidance for the GPU power and the motherboard connectors and used the correct EVGA cables for those.",{"type":23,"tag":24,"props":5431,"children":5432},{},[5433],{"type":28,"value":5434},"But for the SATA power cables, I grabbed the Corsair ones.",{"type":23,"tag":24,"props":5436,"children":5437},{},[5438,5440],{"type":28,"value":5439},"My thinking, stated out loud to no one: ",{"type":23,"tag":5441,"props":5442,"children":5443},"em",{},[5444],{"type":28,"value":5445},"\"SATA can't really be that different, right?\"",{"type":23,"tag":24,"props":5447,"children":5448},{},[5449],{"type":28,"value":5450},"It was. Very different. The drives were dead before the POST screen.",{"type":23,"tag":36,"props":5452,"children":5454},{"id":5453},"why-modular-psu-cables-are-not-interchangeable",[5455],{"type":28,"value":5456},"Why Modular PSU Cables Are Not Interchangeable",{"type":23,"tag":24,"props":5458,"children":5459},{},[5460],{"type":28,"value":5461},"Here's the thing I should have already known, and the thing worth putting in writing so maybe someone else doesn't learn it the same way:",{"type":23,"tag":24,"props":5463,"children":5464},{},[5465,5470],{"type":23,"tag":1392,"props":5466,"children":5467},{},[5468],{"type":28,"value":5469},"The physical connector fitting means nothing.",{"type":28,"value":5471}," The shape and size of a modular PSU cable connector are not standardized on the PSU side. Manufacturers make them look compatible because similar-sized connectors are convenient to produce, not because the pinout matches. You can click a Corsair cable into an EVGA port and it will seat firmly and feel completely correct. Then you turn the power on and the mismatched voltages take out whatever is on the other end.",{"type":23,"tag":24,"props":5473,"children":5474},{},[5475],{"type":28,"value":5476},"SATA power connectors on the device side are standardized. The plug that goes into your drive is always the same. But the cable between your PSU and that plug is carrying voltage assignments that depend entirely on which brand of PSU and which specific model that cable was designed for. Mix them up and you might send 12V to a pin that expects 5V. That's not a warning, it's a component funeral.",{"type":23,"tag":24,"props":5478,"children":5479},{},[5480],{"type":28,"value":5481},"This isn't even a brand-crossing issue exclusively. Some manufacturers have changed their pinout between product generations. Using a cable from one Corsair series on a different Corsair PSU generation can cause the same problem. Always check the compatibility documentation, or better yet, just use the cables that came with the unit.",{"type":23,"tag":36,"props":5483,"children":5485},{"id":5484},"the-other-thing-ai-got-wrong",[5486],{"type":28,"value":5487},"The Other Thing AI Got Wrong",{"type":23,"tag":24,"props":5489,"children":5490},{},[5491],{"type":28,"value":5492},"There is a coda to this story that I almost left out.",{"type":23,"tag":24,"props":5494,"children":5495},{},[5496],{"type":28,"value":5497},"Throughout all of this, the AI was very firm on one point: updating the BIOS was not going to solve anything. The problem was electrical, it said. The PSU rails were collapsing under load. BIOS had nothing to do with it.",{"type":23,"tag":24,"props":5499,"children":5500},{},[5501],{"type":28,"value":5502},"I updated the BIOS anyway, mostly out of desperation.",{"type":23,"tag":24,"props":5504,"children":5505},{},[5506],{"type":28,"value":5507},"The 2014 Gigabyte board had a BIOS update that included revised PCIe link negotiation settings. The RTX 3050, a much newer card, had been failing to negotiate a stable PCIe connection with the old board. After the update, it came up without any issues.",{"type":23,"tag":24,"props":5509,"children":5510},{},[5511],{"type":28,"value":5512},"The AI was wrong about BIOS. I was wrong about SATA cables. We both made mistakes. The difference is I fried hardware.",{"type":23,"tag":36,"props":5514,"children":5516},{"id":5515},"what-i-actually-took-away",[5517],{"type":28,"value":5518},"What I Actually Took Away",{"type":23,"tag":24,"props":5520,"children":5521},{},[5522],{"type":28,"value":5523},"There are three lessons here, and I already knew all three of them.",{"type":23,"tag":24,"props":5525,"children":5526},{},[5527],{"type":28,"value":5528},"The first: trust the warning labels. The AI told me not to mix cable brands. I trusted that guidance for the parts I was more worried about, and I hand-waved it for the part that seemed low-stakes. SATA connectors are boring and ubiquitous and surely they couldn't matter that much. Turns out the stakes are the same regardless of which end of the PC you're working on.",{"type":23,"tag":24,"props":5530,"children":5531},{},[5532],{"type":28,"value":5533},"The second: AI is a tool, not an oracle. It gave me a correct warning that I ignored, and it gave me confident wrong information that I also acted on. The value is in knowing which parts to verify independently. The answer to \"should I update the BIOS\" should never just be \"the AI said no.\" That's a two-minute check with actual patch notes.",{"type":23,"tag":24,"props":5535,"children":5536},{},[5537],{"type":28,"value":5538},"The third, and honestly the one that stings the most: back up everything. I lost all the custom code I had built for the solar inverter polling and the web interface. I can recreate it now that I know what I'm doing, maybe better than the first version. But it still burns to destroy your own work through carelessness. I knew this rule before I knew what a computer was. I still didn't follow it.",{"type":23,"tag":24,"props":5540,"children":5541},{},[5542],{"type":28,"value":5543},"New SSD is in. The rig is alive. Nothing is set up on it yet, but the 3050 posts and the network card came up after the BIOS update.",{"type":23,"tag":24,"props":5545,"children":5546},{},[5547],{"type":28,"value":5548},"Progress. Expensive progress.",{"title":8,"searchDepth":169,"depth":169,"links":5550},[5551,5552,5553,5554,5555,5556],{"id":5354,"depth":169,"text":5357},{"id":5380,"depth":169,"text":5383},{"id":5411,"depth":169,"text":5414},{"id":5453,"depth":169,"text":5456},{"id":5484,"depth":169,"text":5487},{"id":5515,"depth":169,"text":5518},"content:blog:well-i-embarrassed-myself-even-sooner-than-expected-a-modular-psu-cables-tale.md","blog/well-i-embarrassed-myself-even-sooner-than-expected-a-modular-psu-cables-tale.md","blog/well-i-embarrassed-myself-even-sooner-than-expected-a-modular-psu-cables-tale",1774200455342]