[{"data":1,"prerenderedAt":1046},["ShallowReactive",2],{"blog-/blog/home-assistant-mqtt-and-the-timezone-confusion-i-made-myself":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"updated":10,"tags":11,"readingTime":17,"cover":7,"body":18,"_type":1040,"_id":1041,"_source":1042,"_file":1043,"_stem":1044,"_extension":1045},"/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",[12,13,14,15,16],"home-assistant","mqtt","homelab","solar-tracker","timescaledb","7 min read",{"type":19,"children":20,"toc":1031},"root",[21,29,34,41,55,76,82,87,92,98,103,118,123,143,148,376,381,394,400,405,410,415,421,426,431,436,449,530,535,541,546,572,577,626,661,793,844,870,890,942,947,991,1004,1010,1015,1020,1025],{"type":22,"tag":23,"props":24,"children":25},"element","p",{},[26],{"type":27,"value":28},"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":22,"tag":23,"props":30,"children":31},{},[32],{"type":27,"value":33},"That changed this past weekend.",{"type":22,"tag":35,"props":36,"children":38},"h2",{"id":37},"why-i-came-back",[39],{"type":27,"value":40},"Why I Came Back",{"type":22,"tag":23,"props":42,"children":43},{},[44,46,53],{"type":27,"value":45},"The short version is solar monitoring. My Raspberry Pi running Solar Assistant died a while back, and then, as I covered ",{"type":22,"tag":47,"props":48,"children":50},"a",{"href":49},"/blog/well-i-embarrassed-myself-even-sooner-than-expected-a-modular-psu-cables-tale",[51],{"type":27,"value":52},"in my last post",{"type":27,"value":54},", 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":22,"tag":23,"props":56,"children":57},{},[58,60,66,68,74],{"type":27,"value":59},"The longer version is in ",{"type":22,"tag":47,"props":61,"children":63},{"href":62},"/blog/solar-monitoring-part-1-the-python-build",[64],{"type":27,"value":65},"part one",{"type":27,"value":67}," and ",{"type":22,"tag":47,"props":69,"children":71},{"href":70},"/blog/solar-monitoring-part-2-the-typescript-rebuild",[72],{"type":27,"value":73},"part two",{"type":27,"value":75}," 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":22,"tag":35,"props":77,"children":79},{"id":78},"the-setup-wasnt-what-i-expected",[80],{"type":27,"value":81},"The Setup Wasn't What I Expected",{"type":22,"tag":23,"props":83,"children":84},{},[85],{"type":27,"value":86},"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":22,"tag":23,"props":88,"children":89},{},[90],{"type":27,"value":91},"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":22,"tag":35,"props":93,"children":95},{"id":94},"mqtt-finally-made-sense",[96],{"type":27,"value":97},"MQTT Finally Made Sense",{"type":22,"tag":23,"props":99,"children":100},{},[101],{"type":27,"value":102},"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":22,"tag":23,"props":104,"children":105},{},[106,108,116],{"type":27,"value":107},"Home Assistant changes that dynamic. When you install the Mosquitto broker add-on and connect the MQTT integration, the ",{"type":22,"tag":47,"props":109,"children":113},{"href":110,"rel":111},"https://www.home-assistant.io/integrations/mqtt/#mqtt-discovery",[112],"nofollow",[114],{"type":27,"value":115},"MQTT auto-discovery",{"type":27,"value":117}," 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":22,"tag":23,"props":119,"children":120},{},[121],{"type":27,"value":122},"The discovery topic pattern looks like this:",{"type":22,"tag":124,"props":125,"children":128},"pre",{"className":126,"code":127,"language":27,"meta":7,"style":7},"language-text shiki shiki-themes github-dark","homeassistant/sensor/\u003Cdevice-id>/\u003Cmetric>/config\n",[129],{"type":22,"tag":130,"props":131,"children":132},"code",{"__ignoreMap":7},[133],{"type":22,"tag":134,"props":135,"children":138},"span",{"class":136,"line":137},"line",1,[139],{"type":22,"tag":134,"props":140,"children":141},{},[142],{"type":27,"value":127},{"type":22,"tag":23,"props":144,"children":145},{},[146],{"type":27,"value":147},"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":22,"tag":124,"props":149,"children":153},{"className":150,"code":151,"language":152,"meta":7,"style":7},"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",[154],{"type":22,"tag":130,"props":155,"children":156},{"__ignoreMap":7},[157,166,192,214,236,258,280,302,316,340,358,367],{"type":22,"tag":134,"props":158,"children":159},{"class":136,"line":137},[160],{"type":22,"tag":134,"props":161,"children":163},{"style":162},"--shiki-default:#E1E4E8",[164],{"type":27,"value":165},"{\n",{"type":22,"tag":134,"props":167,"children":169},{"class":136,"line":168},2,[170,176,181,187],{"type":22,"tag":134,"props":171,"children":173},{"style":172},"--shiki-default:#79B8FF",[174],{"type":27,"value":175},"  \"name\"",{"type":22,"tag":134,"props":177,"children":178},{"style":162},[179],{"type":27,"value":180},": ",{"type":22,"tag":134,"props":182,"children":184},{"style":183},"--shiki-default:#9ECBFF",[185],{"type":27,"value":186},"\"battery_soc\"",{"type":22,"tag":134,"props":188,"children":189},{"style":162},[190],{"type":27,"value":191},",\n",{"type":22,"tag":134,"props":193,"children":195},{"class":136,"line":194},3,[196,201,205,210],{"type":22,"tag":134,"props":197,"children":198},{"style":172},[199],{"type":27,"value":200},"  \"state_topic\"",{"type":22,"tag":134,"props":202,"children":203},{"style":162},[204],{"type":27,"value":180},{"type":22,"tag":134,"props":206,"children":207},{"style":183},[208],{"type":27,"value":209},"\"solar/battery_soc\"",{"type":22,"tag":134,"props":211,"children":212},{"style":162},[213],{"type":27,"value":191},{"type":22,"tag":134,"props":215,"children":217},{"class":136,"line":216},4,[218,223,227,232],{"type":22,"tag":134,"props":219,"children":220},{"style":172},[221],{"type":27,"value":222},"  \"unit_of_measurement\"",{"type":22,"tag":134,"props":224,"children":225},{"style":162},[226],{"type":27,"value":180},{"type":22,"tag":134,"props":228,"children":229},{"style":183},[230],{"type":27,"value":231},"\"%\"",{"type":22,"tag":134,"props":233,"children":234},{"style":162},[235],{"type":27,"value":191},{"type":22,"tag":134,"props":237,"children":239},{"class":136,"line":238},5,[240,245,249,254],{"type":22,"tag":134,"props":241,"children":242},{"style":172},[243],{"type":27,"value":244},"  \"device_class\"",{"type":22,"tag":134,"props":246,"children":247},{"style":162},[248],{"type":27,"value":180},{"type":22,"tag":134,"props":250,"children":251},{"style":183},[252],{"type":27,"value":253},"\"battery\"",{"type":22,"tag":134,"props":255,"children":256},{"style":162},[257],{"type":27,"value":191},{"type":22,"tag":134,"props":259,"children":261},{"class":136,"line":260},6,[262,267,271,276],{"type":22,"tag":134,"props":263,"children":264},{"style":172},[265],{"type":27,"value":266},"  \"state_class\"",{"type":22,"tag":134,"props":268,"children":269},{"style":162},[270],{"type":27,"value":180},{"type":22,"tag":134,"props":272,"children":273},{"style":183},[274],{"type":27,"value":275},"\"measurement\"",{"type":22,"tag":134,"props":277,"children":278},{"style":162},[279],{"type":27,"value":191},{"type":22,"tag":134,"props":281,"children":283},{"class":136,"line":282},7,[284,289,293,298],{"type":22,"tag":134,"props":285,"children":286},{"style":172},[287],{"type":27,"value":288},"  \"unique_id\"",{"type":22,"tag":134,"props":290,"children":291},{"style":162},[292],{"type":27,"value":180},{"type":22,"tag":134,"props":294,"children":295},{"style":183},[296],{"type":27,"value":297},"\"sph5048_battery_soc\"",{"type":22,"tag":134,"props":299,"children":300},{"style":162},[301],{"type":27,"value":191},{"type":22,"tag":134,"props":303,"children":305},{"class":136,"line":304},8,[306,311],{"type":22,"tag":134,"props":307,"children":308},{"style":172},[309],{"type":27,"value":310},"  \"device\"",{"type":22,"tag":134,"props":312,"children":313},{"style":162},[314],{"type":27,"value":315},": {\n",{"type":22,"tag":134,"props":317,"children":319},{"class":136,"line":318},9,[320,325,330,335],{"type":22,"tag":134,"props":321,"children":322},{"style":172},[323],{"type":27,"value":324},"    \"identifiers\"",{"type":22,"tag":134,"props":326,"children":327},{"style":162},[328],{"type":27,"value":329},": [",{"type":22,"tag":134,"props":331,"children":332},{"style":183},[333],{"type":27,"value":334},"\"sph5048\"",{"type":22,"tag":134,"props":336,"children":337},{"style":162},[338],{"type":27,"value":339},"],\n",{"type":22,"tag":134,"props":341,"children":343},{"class":136,"line":342},10,[344,349,353],{"type":22,"tag":134,"props":345,"children":346},{"style":172},[347],{"type":27,"value":348},"    \"name\"",{"type":22,"tag":134,"props":350,"children":351},{"style":162},[352],{"type":27,"value":180},{"type":22,"tag":134,"props":354,"children":355},{"style":183},[356],{"type":27,"value":357},"\"Solar Inverter\"\n",{"type":22,"tag":134,"props":359,"children":361},{"class":136,"line":360},11,[362],{"type":22,"tag":134,"props":363,"children":364},{"style":162},[365],{"type":27,"value":366},"  }\n",{"type":22,"tag":134,"props":368,"children":370},{"class":136,"line":369},12,[371],{"type":22,"tag":134,"props":372,"children":373},{"style":162},[374],{"type":27,"value":375},"}\n",{"type":22,"tag":23,"props":377,"children":378},{},[379],{"type":27,"value":380},"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":22,"tag":23,"props":382,"children":383},{},[384,386,392],{"type":27,"value":385},"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":22,"tag":130,"props":387,"children":389},{"className":388},[],[390],{"type":27,"value":391},"solar/#",{"type":27,"value":393}," on the broker. Multiple consumers, one publisher, no coupling between them.",{"type":22,"tag":35,"props":395,"children":397},{"id":396},"the-data-looked-wrong",[398],{"type":27,"value":399},"The Data Looked Wrong",{"type":22,"tag":23,"props":401,"children":402},{},[403],{"type":27,"value":404},"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":22,"tag":23,"props":406,"children":407},{},[408],{"type":27,"value":409},"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":22,"tag":23,"props":411,"children":412},{},[413],{"type":27,"value":414},"Two separate issues turned out to be responsible, both timezone-related, and both entirely my fault.",{"type":22,"tag":35,"props":416,"children":418},{"id":417},"timezone-issue-one-home-assistant",[419],{"type":27,"value":420},"Timezone Issue One: Home Assistant",{"type":22,"tag":23,"props":422,"children":423},{},[424],{"type":27,"value":425},"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":22,"tag":23,"props":427,"children":428},{},[429],{"type":27,"value":430},"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":22,"tag":23,"props":432,"children":433},{},[434],{"type":27,"value":435},"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":22,"tag":23,"props":437,"children":438},{},[439,441,447],{"type":27,"value":440},"If you run Home Assistant in Docker, the container also needs the right timezone set. Passing ",{"type":22,"tag":130,"props":442,"children":444},{"className":443},[],[445],{"type":27,"value":446},"TZ",{"type":27,"value":448}," as an environment variable in your Compose file handles it:",{"type":22,"tag":124,"props":450,"children":454},{"className":451,"code":452,"language":453,"meta":7,"style":7},"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",[455],{"type":22,"tag":130,"props":456,"children":457},{"__ignoreMap":7},[458,472,484,501,513],{"type":22,"tag":134,"props":459,"children":460},{"class":136,"line":137},[461,467],{"type":22,"tag":134,"props":462,"children":464},{"style":463},"--shiki-default:#85E89D",[465],{"type":27,"value":466},"services",{"type":22,"tag":134,"props":468,"children":469},{"style":162},[470],{"type":27,"value":471},":\n",{"type":22,"tag":134,"props":473,"children":474},{"class":136,"line":168},[475,480],{"type":22,"tag":134,"props":476,"children":477},{"style":463},[478],{"type":27,"value":479},"  homeassistant",{"type":22,"tag":134,"props":481,"children":482},{"style":162},[483],{"type":27,"value":471},{"type":22,"tag":134,"props":485,"children":486},{"class":136,"line":194},[487,492,496],{"type":22,"tag":134,"props":488,"children":489},{"style":463},[490],{"type":27,"value":491},"    image",{"type":22,"tag":134,"props":493,"children":494},{"style":162},[495],{"type":27,"value":180},{"type":22,"tag":134,"props":497,"children":498},{"style":183},[499],{"type":27,"value":500},"ghcr.io/home-assistant/home-assistant:stable\n",{"type":22,"tag":134,"props":502,"children":503},{"class":136,"line":216},[504,509],{"type":22,"tag":134,"props":505,"children":506},{"style":463},[507],{"type":27,"value":508},"    environment",{"type":22,"tag":134,"props":510,"children":511},{"style":162},[512],{"type":27,"value":471},{"type":22,"tag":134,"props":514,"children":515},{"class":136,"line":238},[516,521,525],{"type":22,"tag":134,"props":517,"children":518},{"style":463},[519],{"type":27,"value":520},"      TZ",{"type":22,"tag":134,"props":522,"children":523},{"style":162},[524],{"type":27,"value":180},{"type":22,"tag":134,"props":526,"children":527},{"style":183},[528],{"type":27,"value":529},"America/New_York\n",{"type":22,"tag":23,"props":531,"children":532},{},[533],{"type":27,"value":534},"The UI setting and the container environment variable are separate things. Both need to be correct.",{"type":22,"tag":35,"props":536,"children":538},{"id":537},"timezone-issue-two-timescaledb",[539],{"type":27,"value":540},"Timezone Issue Two: TimescaleDB",{"type":22,"tag":23,"props":542,"children":543},{},[544],{"type":27,"value":545},"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":22,"tag":23,"props":547,"children":548},{},[549,551,562,564,570],{"type":27,"value":550},"The problem: TimescaleDB's ",{"type":22,"tag":47,"props":552,"children":555},{"href":553,"rel":554},"https://docs.timescale.com/api/latest/hyperfunctions/time_bucket/",[112],[556],{"type":22,"tag":130,"props":557,"children":559},{"className":558},[],[560],{"type":27,"value":561},"time_bucket",{"type":27,"value":563}," function, when used on ",{"type":22,"tag":130,"props":565,"children":567},{"className":566},[],[568],{"type":27,"value":569},"TIMESTAMPTZ",{"type":27,"value":571}," 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":22,"tag":23,"props":573,"children":574},{},[575],{"type":27,"value":576},"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":22,"tag":124,"props":578,"children":582},{"className":579,"code":580,"language":581,"meta":7,"style":7},"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",[583],{"type":22,"tag":130,"props":584,"children":585},{"__ignoreMap":7},[586,594,602,610,618],{"type":22,"tag":134,"props":587,"children":588},{"class":136,"line":137},[589],{"type":22,"tag":134,"props":590,"children":591},{},[592],{"type":27,"value":593},"-- This buckets by UTC midnight, which may not be what you want\n",{"type":22,"tag":134,"props":595,"children":596},{"class":136,"line":168},[597],{"type":22,"tag":134,"props":598,"children":599},{},[600],{"type":27,"value":601},"SELECT time_bucket('1 day', ts) AS bucket, sum(value)\n",{"type":22,"tag":134,"props":603,"children":604},{"class":136,"line":194},[605],{"type":22,"tag":134,"props":606,"children":607},{},[608],{"type":27,"value":609},"FROM inverter_metrics\n",{"type":22,"tag":134,"props":611,"children":612},{"class":136,"line":216},[613],{"type":22,"tag":134,"props":614,"children":615},{},[616],{"type":27,"value":617},"WHERE key = 'pv1_energy_kwh'\n",{"type":22,"tag":134,"props":619,"children":620},{"class":136,"line":238},[621],{"type":22,"tag":134,"props":622,"children":623},{},[624],{"type":27,"value":625},"GROUP BY bucket;\n",{"type":22,"tag":23,"props":627,"children":628},{},[629,631,636,638,644,646,652,654,659],{"type":27,"value":630},"The fix has two parts. First, I set the ",{"type":22,"tag":130,"props":632,"children":634},{"className":633},[],[635],{"type":27,"value":446},{"type":27,"value":637}," environment variable in my Docker Compose ",{"type":22,"tag":130,"props":639,"children":641},{"className":640},[],[642],{"type":27,"value":643},".env",{"type":27,"value":645}," file and passed it through to the TimescaleDB container. PostgreSQL uses this for ",{"type":22,"tag":130,"props":647,"children":649},{"className":648},[],[650],{"type":27,"value":651},"CURRENT_TIMESTAMP",{"type":27,"value":653},", display formatting, and any operations that reference the session timezone — good baseline hygiene, but it doesn't actually move where ",{"type":22,"tag":130,"props":655,"children":657},{"className":656},[],[658],{"type":27,"value":561},{"type":27,"value":660}," draws its day boundaries.",{"type":22,"tag":124,"props":662,"children":664},{"className":451,"code":663,"language":453,"meta":7,"style":7},"# 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",[665],{"type":22,"tag":130,"props":666,"children":667},{"__ignoreMap":7},[668,677,688,700,716,727,743,760,776],{"type":22,"tag":134,"props":669,"children":670},{"class":136,"line":137},[671],{"type":22,"tag":134,"props":672,"children":674},{"style":673},"--shiki-default:#6A737D",[675],{"type":27,"value":676},"# docker-compose.yml\n",{"type":22,"tag":134,"props":678,"children":679},{"class":136,"line":168},[680,684],{"type":22,"tag":134,"props":681,"children":682},{"style":463},[683],{"type":27,"value":466},{"type":22,"tag":134,"props":685,"children":686},{"style":162},[687],{"type":27,"value":471},{"type":22,"tag":134,"props":689,"children":690},{"class":136,"line":194},[691,696],{"type":22,"tag":134,"props":692,"children":693},{"style":463},[694],{"type":27,"value":695},"  timescaledb",{"type":22,"tag":134,"props":697,"children":698},{"style":162},[699],{"type":27,"value":471},{"type":22,"tag":134,"props":701,"children":702},{"class":136,"line":216},[703,707,711],{"type":22,"tag":134,"props":704,"children":705},{"style":463},[706],{"type":27,"value":491},{"type":22,"tag":134,"props":708,"children":709},{"style":162},[710],{"type":27,"value":180},{"type":22,"tag":134,"props":712,"children":713},{"style":183},[714],{"type":27,"value":715},"timescale/timescaledb:latest-pg16\n",{"type":22,"tag":134,"props":717,"children":718},{"class":136,"line":238},[719,723],{"type":22,"tag":134,"props":720,"children":721},{"style":463},[722],{"type":27,"value":508},{"type":22,"tag":134,"props":724,"children":725},{"style":162},[726],{"type":27,"value":471},{"type":22,"tag":134,"props":728,"children":729},{"class":136,"line":260},[730,734,738],{"type":22,"tag":134,"props":731,"children":732},{"style":463},[733],{"type":27,"value":520},{"type":22,"tag":134,"props":735,"children":736},{"style":162},[737],{"type":27,"value":180},{"type":22,"tag":134,"props":739,"children":740},{"style":183},[741],{"type":27,"value":742},"${TZ}\n",{"type":22,"tag":134,"props":744,"children":745},{"class":136,"line":282},[746,751,755],{"type":22,"tag":134,"props":747,"children":748},{"style":463},[749],{"type":27,"value":750},"      POSTGRES_DB",{"type":22,"tag":134,"props":752,"children":753},{"style":162},[754],{"type":27,"value":180},{"type":22,"tag":134,"props":756,"children":757},{"style":183},[758],{"type":27,"value":759},"solar\n",{"type":22,"tag":134,"props":761,"children":762},{"class":136,"line":304},[763,768,772],{"type":22,"tag":134,"props":764,"children":765},{"style":463},[766],{"type":27,"value":767},"      POSTGRES_USER",{"type":22,"tag":134,"props":769,"children":770},{"style":162},[771],{"type":27,"value":180},{"type":22,"tag":134,"props":773,"children":774},{"style":183},[775],{"type":27,"value":759},{"type":22,"tag":134,"props":777,"children":778},{"class":136,"line":318},[779,784,788],{"type":22,"tag":134,"props":780,"children":781},{"style":463},[782],{"type":27,"value":783},"      POSTGRES_PASSWORD",{"type":22,"tag":134,"props":785,"children":786},{"style":162},[787],{"type":27,"value":180},{"type":22,"tag":134,"props":789,"children":790},{"style":183},[791],{"type":27,"value":792},"${DB_PASSWORD}\n",{"type":22,"tag":124,"props":794,"children":798},{"className":795,"code":796,"language":797,"meta":7,"style":7},"language-bash shiki shiki-themes github-dark","# .env\nTZ=America/New_York\nDB_PASSWORD=...\n","bash",[799],{"type":22,"tag":130,"props":800,"children":801},{"__ignoreMap":7},[802,810,827],{"type":22,"tag":134,"props":803,"children":804},{"class":136,"line":137},[805],{"type":22,"tag":134,"props":806,"children":807},{"style":673},[808],{"type":27,"value":809},"# .env\n",{"type":22,"tag":134,"props":811,"children":812},{"class":136,"line":168},[813,817,823],{"type":22,"tag":134,"props":814,"children":815},{"style":162},[816],{"type":27,"value":446},{"type":22,"tag":134,"props":818,"children":820},{"style":819},"--shiki-default:#F97583",[821],{"type":27,"value":822},"=",{"type":22,"tag":134,"props":824,"children":825},{"style":183},[826],{"type":27,"value":529},{"type":22,"tag":134,"props":828,"children":829},{"class":136,"line":194},[830,835,839],{"type":22,"tag":134,"props":831,"children":832},{"style":162},[833],{"type":27,"value":834},"DB_PASSWORD",{"type":22,"tag":134,"props":836,"children":837},{"style":819},[838],{"type":27,"value":822},{"type":22,"tag":134,"props":840,"children":841},{"style":183},[842],{"type":27,"value":843},"...\n",{"type":22,"tag":23,"props":845,"children":846},{},[847,849,854,856,861,863,868],{"type":27,"value":848},"On ",{"type":22,"tag":130,"props":850,"children":852},{"className":851},[],[853],{"type":27,"value":569},{"type":27,"value":855}," columns, ",{"type":22,"tag":130,"props":857,"children":859},{"className":858},[],[860],{"type":27,"value":561},{"type":27,"value":862}," always buckets by UTC midnight regardless of the container's ",{"type":22,"tag":130,"props":864,"children":866},{"className":865},[],[867],{"type":27,"value":446},{"type":27,"value":869}," setting. The env var helps with display; the query itself needs fixing.",{"type":22,"tag":23,"props":871,"children":872},{},[873,875,880,882,888],{"type":27,"value":874},"For local-midnight bucketing, the ",{"type":22,"tag":130,"props":876,"children":878},{"className":877},[],[879],{"type":27,"value":561},{"type":27,"value":881}," function has an ",{"type":22,"tag":130,"props":883,"children":885},{"className":884},[],[886],{"type":27,"value":887},"origin",{"type":27,"value":889}," parameter that shifts the bucket start point:",{"type":22,"tag":124,"props":891,"children":893},{"className":579,"code":892,"language":581,"meta":7,"style":7},"-- 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",[894],{"type":22,"tag":130,"props":895,"children":896},{"__ignoreMap":7},[897,905,913,921,928,935],{"type":22,"tag":134,"props":898,"children":899},{"class":136,"line":137},[900],{"type":22,"tag":134,"props":901,"children":902},{},[903],{"type":27,"value":904},"-- Shift buckets to align with local midnight (UTC-5 example)\n",{"type":22,"tag":134,"props":906,"children":907},{"class":136,"line":168},[908],{"type":22,"tag":134,"props":909,"children":910},{},[911],{"type":27,"value":912},"SELECT time_bucket('1 day', ts, '2026-01-01 00:00:00-05'::timestamptz) AS bucket,\n",{"type":22,"tag":134,"props":914,"children":915},{"class":136,"line":194},[916],{"type":22,"tag":134,"props":917,"children":918},{},[919],{"type":27,"value":920},"       sum(value)\n",{"type":22,"tag":134,"props":922,"children":923},{"class":136,"line":216},[924],{"type":22,"tag":134,"props":925,"children":926},{},[927],{"type":27,"value":609},{"type":22,"tag":134,"props":929,"children":930},{"class":136,"line":238},[931],{"type":22,"tag":134,"props":932,"children":933},{},[934],{"type":27,"value":617},{"type":22,"tag":134,"props":936,"children":937},{"class":136,"line":260},[938],{"type":22,"tag":134,"props":939,"children":940},{},[941],{"type":27,"value":625},{"type":22,"tag":23,"props":943,"children":944},{},[945],{"type":27,"value":946},"Or you can cast the timestamp to a specific timezone before bucketing:",{"type":22,"tag":124,"props":948,"children":950},{"className":579,"code":949,"language":581,"meta":7,"style":7},"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",[951],{"type":22,"tag":130,"props":952,"children":953},{"__ignoreMap":7},[954,962,969,976,983],{"type":22,"tag":134,"props":955,"children":956},{"class":136,"line":137},[957],{"type":22,"tag":134,"props":958,"children":959},{},[960],{"type":27,"value":961},"SELECT time_bucket('1 day', ts AT TIME ZONE 'America/New_York') AS local_bucket,\n",{"type":22,"tag":134,"props":963,"children":964},{"class":136,"line":168},[965],{"type":22,"tag":134,"props":966,"children":967},{},[968],{"type":27,"value":920},{"type":22,"tag":134,"props":970,"children":971},{"class":136,"line":194},[972],{"type":22,"tag":134,"props":973,"children":974},{},[975],{"type":27,"value":609},{"type":22,"tag":134,"props":977,"children":978},{"class":136,"line":216},[979],{"type":22,"tag":134,"props":980,"children":981},{},[982],{"type":27,"value":617},{"type":22,"tag":134,"props":984,"children":985},{"class":136,"line":238},[986],{"type":22,"tag":134,"props":987,"children":988},{},[989],{"type":27,"value":990},"GROUP BY local_bucket;\n",{"type":22,"tag":23,"props":992,"children":993},{},[994,996,1002],{"type":27,"value":995},"The ",{"type":22,"tag":130,"props":997,"children":999},{"className":998},[],[1000],{"type":27,"value":1001},"AT TIME ZONE",{"type":27,"value":1003}," 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":22,"tag":35,"props":1005,"children":1007},{"id":1006},"what-this-weekend-produced",[1008],{"type":27,"value":1009},"What This Weekend Produced",{"type":22,"tag":23,"props":1011,"children":1012},{},[1013],{"type":27,"value":1014},"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":22,"tag":23,"props":1016,"children":1017},{},[1018],{"type":27,"value":1019},"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":22,"tag":23,"props":1021,"children":1022},{},[1023],{"type":27,"value":1024},"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":22,"tag":1026,"props":1027,"children":1028},"style",{},[1029],{"type":27,"value":1030},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":7,"searchDepth":168,"depth":168,"links":1032},[1033,1034,1035,1036,1037,1038,1039],{"id":37,"depth":168,"text":40},{"id":78,"depth":168,"text":81},{"id":94,"depth":168,"text":97},{"id":396,"depth":168,"text":399},{"id":417,"depth":168,"text":420},{"id":537,"depth":168,"text":540},{"id":1006,"depth":168,"text":1009},"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",1774200455342]