mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-29 21:03:40 +00:00
1 line
No EOL
31 KiB
JavaScript
1 line
No EOL
31 KiB
JavaScript
"use strict";(globalThis.webpackChunkdocs_split_developer=globalThis.webpackChunkdocs_split_developer||[]).push([[2443],{936:(e,n,r)=>{r.r(n),r.d(n,{assets:()=>d,contentTitle:()=>l,default:()=>h,frontMatter:()=>c,metadata:()=>i,toc:()=>a});const i=JSON.parse('{"id":"architecture","title":"Architecture","description":"This document provides a visual overview of the integration\'s architecture, focusing on end-to-end data flow and caching layers.","source":"@site/docs/architecture.md","sourceDirName":".","slug":"/architecture","permalink":"/hass.tibber_prices/developer/architecture","draft":false,"unlisted":false,"editUrl":"https://github.com/jpawlowski/hass.tibber_prices/tree/main/docs/developer/docs/architecture.md","tags":[],"version":"current","lastUpdatedAt":1764985026000,"frontMatter":{"comments":false},"sidebar":"tutorialSidebar","previous":{"title":"Developer Documentation","permalink":"/hass.tibber_prices/developer/intro"},"next":{"title":"Timer Architecture","permalink":"/hass.tibber_prices/developer/timer-architecture"}}');var s=r(4848),t=r(8453);const c={comments:!1},l="Architecture",d={},a=[{value:"End-to-End Data Flow",id:"end-to-end-data-flow",level:2},{value:"Flow Description",id:"flow-description",level:3},{value:"Caching Architecture",id:"caching-architecture",level:2},{value:"Overview",id:"overview",level:3},{value:"Cache Coordination",id:"cache-coordination",level:3},{value:"Component Responsibilities",id:"component-responsibilities",level:2},{value:"Core Components",id:"core-components",level:3},{value:"Sensor Architecture (Calculator Pattern)",id:"sensor-architecture-calculator-pattern",level:3},{value:"Helper Utilities",id:"helper-utilities",level:3},{value:"Key Patterns",id:"key-patterns",level:2},{value:"1. Dual Translation System",id:"1-dual-translation-system",level:3},{value:"2. Price Data Enrichment",id:"2-price-data-enrichment",level:3},{value:"3. Quarter-Hour Precision",id:"3-quarter-hour-precision",level:3},{value:"4. Calculator Pattern (Sensor Platform)",id:"4-calculator-pattern-sensor-platform",level:3},{value:"Performance Characteristics",id:"performance-characteristics",level:2},{value:"API Call Reduction",id:"api-call-reduction",level:3},{value:"CPU Optimization",id:"cpu-optimization",level:3},{value:"Memory Usage",id:"memory-usage",level:3},{value:"Related Documentation",id:"related-documentation",level:2}];function o(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",hr:"hr",li:"li",mermaid:"mermaid",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.header,{children:(0,s.jsx)(n.h1,{id:"architecture",children:"Architecture"})}),"\n",(0,s.jsx)(n.p,{children:"This document provides a visual overview of the integration's architecture, focusing on end-to-end data flow and caching layers."}),"\n",(0,s.jsxs)(n.p,{children:["For detailed implementation patterns, see ",(0,s.jsx)(n.a,{href:"https://github.com/jpawlowski/hass.tibber_prices/blob/v0.20.0/AGENTS.md",children:(0,s.jsx)(n.code,{children:"AGENTS.md"})}),"."]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"end-to-end-data-flow",children:"End-to-End Data Flow"}),"\n",(0,s.jsx)(n.mermaid,{value:'flowchart TB\n %% External Systems\n TIBBER[("\ud83c\udf10 Tibber GraphQL API<br/>api.tibber.com")]\n HA[("\ud83c\udfe0 Home Assistant<br/>Core")]\n\n %% Entry Point\n SETUP["__init__.py<br/>async_setup_entry()"]\n\n %% Core Components\n API["api.py<br/>TibberPricesApiClient<br/><br/>GraphQL queries"]\n COORD["coordinator.py<br/>TibberPricesDataUpdateCoordinator<br/><br/>Orchestrates updates every 15min"]\n\n %% Caching Layers\n CACHE_API["\ud83d\udcbe API Cache<br/>coordinator/cache.py<br/><br/>HA Storage (persistent)<br/>User: 24h | Prices: until midnight"]\n CACHE_TRANS["\ud83d\udcbe Transformation Cache<br/>coordinator/data_transformation.py<br/><br/>Memory (enriched prices)<br/>Until config change or midnight"]\n CACHE_PERIOD["\ud83d\udcbe Period Cache<br/>coordinator/periods.py<br/><br/>Memory (calculated periods)<br/>Hash-based invalidation"]\n CACHE_CONFIG["\ud83d\udcbe Config Cache<br/>coordinator/*<br/><br/>Memory (parsed options)<br/>Until config change"]\n CACHE_TRANS_TEXT["\ud83d\udcbe Translation Cache<br/>const.py<br/><br/>Memory (UI strings)<br/>Until HA restart"]\n\n %% Processing Components\n TRANSFORM["coordinator/data_transformation.py<br/>DataTransformer<br/><br/>Enrich prices with statistics"]\n PERIODS["coordinator/periods.py<br/>PeriodCalculator<br/><br/>Calculate best/peak periods"]\n ENRICH["price_utils.py + average_utils.py<br/><br/>Calculate trailing/leading averages<br/>rating_level, differences"]\n\n %% Output Components\n SENSORS["sensor/<br/>TibberPricesSensor<br/><br/>120+ price/level/rating sensors"]\n BINARY["binary_sensor/<br/>TibberPricesBinarySensor<br/><br/>Period indicators"]\n SERVICES["services/<br/><br/>Custom service endpoints<br/>(get_chartdata, ApexCharts)"]\n\n %% Flow Connections\n TIBBER --\x3e|"Query user data<br/>Query prices<br/>(yesterday/today/tomorrow)"| API\n\n API --\x3e|"Raw GraphQL response"| COORD\n\n COORD --\x3e|"Check cache first"| CACHE_API\n CACHE_API -.->|"Cache hit:<br/>Return cached"| COORD\n CACHE_API -.->|"Cache miss:<br/>Fetch from API"| API\n\n COORD --\x3e|"Raw price data"| TRANSFORM\n TRANSFORM --\x3e|"Check cache"| CACHE_TRANS\n CACHE_TRANS -.->|"Cache hit"| TRANSFORM\n CACHE_TRANS -.->|"Cache miss"| ENRICH\n ENRICH --\x3e|"Enriched data"| TRANSFORM\n\n TRANSFORM --\x3e|"Enriched price data"| COORD\n\n COORD --\x3e|"Enriched data"| PERIODS\n PERIODS --\x3e|"Check cache"| CACHE_PERIOD\n CACHE_PERIOD -.->|"Hash match:<br/>Return cached"| PERIODS\n CACHE_PERIOD -.->|"Hash mismatch:<br/>Recalculate"| PERIODS\n\n PERIODS --\x3e|"Calculated periods"| COORD\n\n COORD --\x3e|"Complete data<br/>(prices + periods)"| SENSORS\n COORD --\x3e|"Complete data"| BINARY\n COORD --\x3e|"Data access"| SERVICES\n\n SENSORS --\x3e|"Entity states"| HA\n BINARY --\x3e|"Entity states"| HA\n SERVICES --\x3e|"Service responses"| HA\n\n %% Config access\n CACHE_CONFIG -.->|"Parsed options"| TRANSFORM\n CACHE_CONFIG -.->|"Parsed options"| PERIODS\n CACHE_TRANS_TEXT -.->|"UI strings"| SENSORS\n CACHE_TRANS_TEXT -.->|"UI strings"| BINARY\n\n SETUP --\x3e|"Initialize"| COORD\n SETUP --\x3e|"Register"| SENSORS\n SETUP --\x3e|"Register"| BINARY\n SETUP --\x3e|"Register"| SERVICES\n\n %% Styling\n classDef external fill:#e1f5ff,stroke:#0288d1,stroke-width:3px\n classDef cache fill:#fff3e0,stroke:#f57c00,stroke-width:2px\n classDef processing fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px\n classDef output fill:#e8f5e9,stroke:#388e3c,stroke-width:2px\n\n class TIBBER,HA external\n class CACHE_API,CACHE_TRANS,CACHE_PERIOD,CACHE_CONFIG,CACHE_TRANS_TEXT cache\n class TRANSFORM,PERIODS,ENRICH processing\n class SENSORS,BINARY,SERVICES output'}),"\n",(0,s.jsx)(n.h3,{id:"flow-description",children:"Flow Description"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Setup"})," (",(0,s.jsx)(n.code,{children:"__init__.py"}),")"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Integration loads, creates coordinator instance"}),"\n",(0,s.jsx)(n.li,{children:"Registers entity platforms (sensor, binary_sensor)"}),"\n",(0,s.jsx)(n.li,{children:"Sets up custom services"}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Data Fetch"})," (every 15 minutes)"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["Coordinator triggers update via ",(0,s.jsx)(n.code,{children:"api.py"})]}),"\n",(0,s.jsxs)(n.li,{children:["API client checks ",(0,s.jsx)(n.strong,{children:"persistent cache"})," first (",(0,s.jsx)(n.code,{children:"coordinator/cache.py"}),")"]}),"\n",(0,s.jsx)(n.li,{children:"If cache valid \u2192 return cached data"}),"\n",(0,s.jsx)(n.li,{children:"If cache stale \u2192 query Tibber GraphQL API"}),"\n",(0,s.jsx)(n.li,{children:"Store fresh data in persistent cache (survives HA restart)"}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Price Enrichment"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["Coordinator passes raw prices to ",(0,s.jsx)(n.code,{children:"DataTransformer"})]}),"\n",(0,s.jsxs)(n.li,{children:["Transformer checks ",(0,s.jsx)(n.strong,{children:"transformation cache"})," (memory)"]}),"\n",(0,s.jsx)(n.li,{children:"If cache valid \u2192 return enriched data"}),"\n",(0,s.jsxs)(n.li,{children:["If cache invalid \u2192 enrich via ",(0,s.jsx)(n.code,{children:"price_utils.py"})," + ",(0,s.jsx)(n.code,{children:"average_utils.py"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Calculate 24h trailing/leading averages"}),"\n",(0,s.jsx)(n.li,{children:"Calculate price differences (% from average)"}),"\n",(0,s.jsx)(n.li,{children:"Assign rating levels (LOW/NORMAL/HIGH)"}),"\n"]}),"\n"]}),"\n",(0,s.jsx)(n.li,{children:"Store enriched data in transformation cache"}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Period Calculation"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["Coordinator passes enriched data to ",(0,s.jsx)(n.code,{children:"PeriodCalculator"})]}),"\n",(0,s.jsxs)(n.li,{children:["Calculator computes ",(0,s.jsx)(n.strong,{children:"hash"})," from prices + config"]}),"\n",(0,s.jsx)(n.li,{children:"If hash matches cache \u2192 return cached periods"}),"\n",(0,s.jsx)(n.li,{children:"If hash differs \u2192 recalculate best/peak price periods"}),"\n",(0,s.jsx)(n.li,{children:"Store periods with new hash"}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Entity Updates"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Coordinator provides complete data (prices + periods)"}),"\n",(0,s.jsx)(n.li,{children:"Sensors read values via unified handlers"}),"\n",(0,s.jsx)(n.li,{children:"Binary sensors evaluate period states"}),"\n",(0,s.jsx)(n.li,{children:"Entities update on quarter-hour boundaries (00/15/30/45)"}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Service Calls"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Custom services access coordinator data directly"}),"\n",(0,s.jsx)(n.li,{children:"Return formatted responses (JSON, ApexCharts format)"}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"caching-architecture",children:"Caching Architecture"}),"\n",(0,s.jsx)(n.h3,{id:"overview",children:"Overview"}),"\n",(0,s.jsxs)(n.p,{children:["The integration uses ",(0,s.jsx)(n.strong,{children:"5 independent caching layers"})," for optimal performance:"]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Layer"}),(0,s.jsx)(n.th,{children:"Location"}),(0,s.jsx)(n.th,{children:"Lifetime"}),(0,s.jsx)(n.th,{children:"Invalidation"}),(0,s.jsx)(n.th,{children:"Memory"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"API Cache"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"coordinator/cache.py"})}),(0,s.jsxs)(n.td,{children:["24h (user)",(0,s.jsx)("br",{}),"Until midnight (prices)"]}),(0,s.jsx)(n.td,{children:"Automatic"}),(0,s.jsx)(n.td,{children:"50KB"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Translation Cache"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"const.py"})}),(0,s.jsx)(n.td,{children:"Until HA restart"}),(0,s.jsx)(n.td,{children:"Never"}),(0,s.jsx)(n.td,{children:"5KB"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Config Cache"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"coordinator/*"})}),(0,s.jsx)(n.td,{children:"Until config change"}),(0,s.jsx)(n.td,{children:"Explicit"}),(0,s.jsx)(n.td,{children:"1KB"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Period Cache"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"coordinator/periods.py"})}),(0,s.jsx)(n.td,{children:"Until data/config change"}),(0,s.jsx)(n.td,{children:"Hash-based"}),(0,s.jsx)(n.td,{children:"10KB"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Transformation Cache"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"coordinator/data_transformation.py"})}),(0,s.jsx)(n.td,{children:"Until midnight/config"}),(0,s.jsx)(n.td,{children:"Automatic"}),(0,s.jsx)(n.td,{children:"60KB"})]})]})]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Total cache overhead:"})," ~126KB per coordinator instance (main entry + subentries)"]}),"\n",(0,s.jsx)(n.h3,{id:"cache-coordination",children:"Cache Coordination"}),"\n",(0,s.jsx)(n.mermaid,{value:'flowchart LR\n USER[("User changes options")]\n MIDNIGHT[("Midnight turnover")]\n NEWDATA[("Tomorrow data arrives")]\n\n USER --\x3e|"Explicit invalidation"| CONFIG["Config Cache<br/>\u274c Clear"]\n USER --\x3e|"Explicit invalidation"| PERIOD["Period Cache<br/>\u274c Clear"]\n USER --\x3e|"Explicit invalidation"| TRANS["Transformation Cache<br/>\u274c Clear"]\n\n MIDNIGHT --\x3e|"Date validation"| API["API Cache<br/>\u274c Clear prices"]\n MIDNIGHT --\x3e|"Date check"| TRANS\n\n NEWDATA --\x3e|"Hash mismatch"| PERIOD\n\n CONFIG -.->|"Next access"| CONFIG_NEW["Reparse options"]\n PERIOD -.->|"Next access"| PERIOD_NEW["Recalculate"]\n TRANS -.->|"Next access"| TRANS_NEW["Re-enrich"]\n API -.->|"Next access"| API_NEW["Fetch from API"]\n\n classDef invalid fill:#ffebee,stroke:#c62828,stroke-width:2px\n classDef rebuild fill:#e8f5e9,stroke:#388e3c,stroke-width:2px\n\n class CONFIG,PERIOD,TRANS,API invalid\n class CONFIG_NEW,PERIOD_NEW,TRANS_NEW,API_NEW rebuild'}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Key insight:"})," No cascading invalidations - each cache is independent and rebuilds on-demand."]}),"\n",(0,s.jsxs)(n.p,{children:["For detailed cache behavior, see ",(0,s.jsx)(n.a,{href:"/hass.tibber_prices/developer/caching-strategy",children:"Caching Strategy"}),"."]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"component-responsibilities",children:"Component Responsibilities"}),"\n",(0,s.jsx)(n.h3,{id:"core-components",children:"Core Components"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Component"}),(0,s.jsx)(n.th,{children:"File"}),(0,s.jsx)(n.th,{children:"Responsibility"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"API Client"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"api.py"})}),(0,s.jsx)(n.td,{children:"GraphQL queries to Tibber, retry logic, error handling"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Coordinator"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"coordinator.py"})}),(0,s.jsx)(n.td,{children:"Update orchestration, cache management, absolute-time scheduling with boundary tolerance"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Data Transformer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"coordinator/data_transformation.py"})}),(0,s.jsx)(n.td,{children:"Price enrichment (averages, ratings, differences)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Period Calculator"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"coordinator/periods.py"})}),(0,s.jsx)(n.td,{children:"Best/peak price period calculation with relaxation"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Sensors"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"sensor/"})}),(0,s.jsx)(n.td,{children:"80+ entities for prices, levels, ratings, statistics"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Binary Sensors"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"binary_sensor/"})}),(0,s.jsx)(n.td,{children:"Period indicators (best/peak price active)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Services"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"services/"})}),(0,s.jsx)(n.td,{children:"Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data)"})]})]})]}),"\n",(0,s.jsx)(n.h3,{id:"sensor-architecture-calculator-pattern",children:"Sensor Architecture (Calculator Pattern)"}),"\n",(0,s.jsxs)(n.p,{children:["The sensor platform uses ",(0,s.jsx)(n.strong,{children:"Calculator Pattern"})," for clean separation of concerns (refactored Nov 2025):"]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Component"}),(0,s.jsx)(n.th,{children:"Files"}),(0,s.jsx)(n.th,{children:"Lines"}),(0,s.jsx)(n.th,{children:"Responsibility"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Entity Class"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"sensor/core.py"})}),(0,s.jsx)(n.td,{children:"909"}),(0,s.jsx)(n.td,{children:"Entity lifecycle, coordinator, delegates to calculators"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Calculators"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"sensor/calculators/"})}),(0,s.jsx)(n.td,{children:"1,838"}),(0,s.jsx)(n.td,{children:"Business logic (8 specialized calculators)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Attributes"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"sensor/attributes/"})}),(0,s.jsx)(n.td,{children:"1,209"}),(0,s.jsx)(n.td,{children:"State presentation (8 specialized modules)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Routing"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"sensor/value_getters.py"})}),(0,s.jsx)(n.td,{children:"276"}),(0,s.jsx)(n.td,{children:"Centralized sensor \u2192 calculator mapping"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Chart Export"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"sensor/chart_data.py"})}),(0,s.jsx)(n.td,{children:"144"}),(0,s.jsx)(n.td,{children:"Service call handling, YAML parsing"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Helpers"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"sensor/helpers.py"})}),(0,s.jsx)(n.td,{children:"188"}),(0,s.jsx)(n.td,{children:"Aggregation functions, utilities"})]})]})]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Calculator Package"})," (",(0,s.jsx)(n.code,{children:"sensor/calculators/"}),"):"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"base.py"})," - Abstract BaseCalculator with coordinator access"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"interval.py"})," - Single interval calculations (current/next/previous)"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"rolling_hour.py"})," - 5-interval rolling windows"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"daily_stat.py"})," - Calendar day min/max/avg statistics"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"window_24h.py"})," - Trailing/leading 24h windows"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"volatility.py"})," - Price volatility analysis"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"trend.py"})," - Complex trend analysis with caching"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"timing.py"})," - Best/peak price period timing"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"metadata.py"})," - Home/metering metadata"]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Benefits:"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"58% reduction in core.py (2,170 \u2192 909 lines)"}),"\n",(0,s.jsx)(n.li,{children:"Clear separation: Calculators (logic) vs Attributes (presentation)"}),"\n",(0,s.jsx)(n.li,{children:"Independent testability for each calculator"}),"\n",(0,s.jsx)(n.li,{children:"Easy to add sensors: Choose calculation pattern, add to routing"}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"helper-utilities",children:"Helper Utilities"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Utility"}),(0,s.jsx)(n.th,{children:"File"}),(0,s.jsx)(n.th,{children:"Purpose"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Price Utils"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"utils/price.py"})}),(0,s.jsx)(n.td,{children:"Rating calculation, enrichment, level aggregation"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Average Utils"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"utils/average.py"})}),(0,s.jsx)(n.td,{children:"Trailing/leading 24h average calculations"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Entity Utils"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"entity_utils/"})}),(0,s.jsx)(n.td,{children:"Shared icon/color/attribute logic"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Translations"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"const.py"})}),(0,s.jsx)(n.td,{children:"Translation loading and caching"})]})]})]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"key-patterns",children:"Key Patterns"}),"\n",(0,s.jsx)(n.h3,{id:"1-dual-translation-system",children:"1. Dual Translation System"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Standard translations"})," (",(0,s.jsx)(n.code,{children:"/translations/*.json"}),"): HA-compliant schema for entity names"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Custom translations"})," (",(0,s.jsx)(n.code,{children:"/custom_translations/*.json"}),"): Extended descriptions, usage tips"]}),"\n",(0,s.jsx)(n.li,{children:"Both loaded at integration setup, cached in memory"}),"\n",(0,s.jsxs)(n.li,{children:["Access via ",(0,s.jsx)(n.code,{children:"get_translation()"})," helper function"]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"2-price-data-enrichment",children:"2. Price Data Enrichment"}),"\n",(0,s.jsxs)(n.p,{children:["All quarter-hourly price intervals get augmented via ",(0,s.jsx)(n.code,{children:"utils/price.py"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'# Original from Tibber API\n{\n "startsAt": "2025-11-03T14:00:00+01:00",\n "total": 0.2534,\n "level": "NORMAL"\n}\n\n# After enrichment (utils/price.py)\n{\n "startsAt": "2025-11-03T14:00:00+01:00",\n "total": 0.2534,\n "level": "NORMAL",\n "trailing_avg_24h": 0.2312, # \u2190 Added: 24h trailing average\n "difference": 9.6, # \u2190 Added: % diff from trailing avg\n "rating_level": "NORMAL" # \u2190 Added: LOW/NORMAL/HIGH based on thresholds\n}\n'})}),"\n",(0,s.jsx)(n.h3,{id:"3-quarter-hour-precision",children:"3. Quarter-Hour Precision"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"API polling"}),": Every 15 minutes (coordinator fetch cycle)"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Entity updates"}),": On 00/15/30/45-minute boundaries via ",(0,s.jsx)(n.code,{children:"coordinator/listeners.py"})]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Timer scheduling"}),": Uses ",(0,s.jsx)(n.code,{children:"async_track_utc_time_change(minute=[0, 15, 30, 45], second=0)"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"HA may trigger \xb1few milliseconds before/after exact boundary"}),"\n",(0,s.jsxs)(n.li,{children:["Smart boundary tolerance (\xb12 seconds) handles scheduling jitter in ",(0,s.jsx)(n.code,{children:"sensor/helpers.py"})]}),"\n",(0,s.jsx)(n.li,{children:"If HA schedules at 14:59:58 \u2192 rounds to 15:00:00 (shows new interval data)"}),"\n",(0,s.jsx)(n.li,{children:"If HA restarts at 14:59:30 \u2192 stays at 14:45:00 (shows current interval data)"}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Absolute time tracking"}),": Timer plans for ",(0,s.jsx)(n.strong,{children:"all future boundaries"})," (not relative delays)","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00)"}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Result"}),": Current price sensors update without waiting for next API poll"]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"4-calculator-pattern-sensor-platform",children:"4. Calculator Pattern (Sensor Platform)"}),"\n",(0,s.jsxs)(n.p,{children:["Sensors organized by ",(0,s.jsx)(n.strong,{children:"calculation method"})," (refactored Nov 2025):"]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Unified Handler Methods"})," (",(0,s.jsx)(n.code,{children:"sensor/core.py"}),"):"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"_get_interval_value(offset, type)"})," - current/next/previous intervals"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"_get_rolling_hour_value(offset, type)"})," - 5-interval rolling windows"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"_get_daily_stat_value(day, stat_func)"})," - calendar day min/max/avg"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"_get_24h_window_value(stat_func)"})," - trailing/leading statistics"]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Routing"})," (",(0,s.jsx)(n.code,{children:"sensor/value_getters.py"}),"):"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Single source of truth mapping 80+ entity keys to calculator methods"}),"\n",(0,s.jsx)(n.li,{children:"Organized by calculation type (Interval, Rolling Hour, Daily Stats, etc.)"}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Calculators"})," (",(0,s.jsx)(n.code,{children:"sensor/calculators/"}),"):"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["Each calculator inherits from ",(0,s.jsx)(n.code,{children:"BaseCalculator"})," with coordinator access"]}),"\n",(0,s.jsxs)(n.li,{children:["Focused responsibility: ",(0,s.jsx)(n.code,{children:"IntervalCalculator"}),", ",(0,s.jsx)(n.code,{children:"TrendCalculator"}),", etc."]}),"\n",(0,s.jsxs)(n.li,{children:["Complex logic isolated (e.g., ",(0,s.jsx)(n.code,{children:"TrendCalculator"})," has internal caching)"]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Attributes"})," (",(0,s.jsx)(n.code,{children:"sensor/attributes/"}),"):"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Separate from business logic, handles state presentation"}),"\n",(0,s.jsx)(n.li,{children:"Builds extra_state_attributes dicts for entity classes"}),"\n",(0,s.jsxs)(n.li,{children:["Unified builders: ",(0,s.jsx)(n.code,{children:"build_sensor_attributes()"}),", ",(0,s.jsx)(n.code,{children:"build_extra_state_attributes()"})]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Benefits:"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Minimal code duplication across 80+ sensors"}),"\n",(0,s.jsx)(n.li,{children:"Clear separation of concerns (calculation vs presentation)"}),"\n",(0,s.jsx)(n.li,{children:"Easy to extend: Add sensor \u2192 choose pattern \u2192 add to routing"}),"\n",(0,s.jsx)(n.li,{children:"Independent testability for each component"}),"\n"]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"performance-characteristics",children:"Performance Characteristics"}),"\n",(0,s.jsx)(n.h3,{id:"api-call-reduction",children:"API Call Reduction"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Without caching:"})," 96 API calls/day (every 15 min)"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"With caching:"})," ~1-2 API calls/day (only when cache expires)"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Reduction:"})," ~98%"]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"cpu-optimization",children:"CPU Optimization"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Optimization"}),(0,s.jsx)(n.th,{children:"Location"}),(0,s.jsx)(n.th,{children:"Savings"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Config caching"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"coordinator/*"})}),(0,s.jsx)(n.td,{children:"~50% on config checks"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Period caching"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"coordinator/periods.py"})}),(0,s.jsx)(n.td,{children:"~70% on period recalculation"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Lazy logging"}),(0,s.jsx)(n.td,{children:"Throughout"}),(0,s.jsx)(n.td,{children:"~15% on log-heavy operations"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Import optimization"}),(0,s.jsx)(n.td,{children:"Module structure"}),(0,s.jsx)(n.td,{children:"~20% faster loading"})]})]})]}),"\n",(0,s.jsx)(n.h3,{id:"memory-usage",children:"Memory Usage"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Per coordinator instance:"})," ~126KB cache overhead"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Typical setup:"})," 1 main + 2 subentries = ~378KB total"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Redundancy eliminated:"})," 14% reduction (10KB saved per coordinator)"]}),"\n"]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"related-documentation",children:"Related Documentation"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:(0,s.jsx)(n.a,{href:"/hass.tibber_prices/developer/timer-architecture",children:"Timer Architecture"})})," - Timer system, scheduling, coordination (3 independent timers)"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:(0,s.jsx)(n.a,{href:"/hass.tibber_prices/developer/caching-strategy",children:"Caching Strategy"})})," - Detailed cache behavior, invalidation, debugging"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:(0,s.jsx)(n.a,{href:"/hass.tibber_prices/developer/setup",children:"Setup Guide"})})," - Development environment setup"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:(0,s.jsx)(n.a,{href:"/hass.tibber_prices/developer/testing",children:"Testing Guide"})})," - How to test changes"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:(0,s.jsx)(n.a,{href:"/hass.tibber_prices/developer/release-management",children:"Release Management"})})," - Release workflow and versioning"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:(0,s.jsx)(n.a,{href:"https://github.com/jpawlowski/hass.tibber_prices/blob/v0.20.0/AGENTS.md",children:"AGENTS.md"})})," - Complete reference for AI development"]}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(o,{...e})}):o(e)}}}]); |