hass.tibber_prices/developer/assets/js/d7c6ee3c.0344cf7a.js
github-actions[bot] e9aea64a2e deploy: 6898c126e3
2025-12-06 01:42:39 +00:00

1 line
No EOL
76 KiB
JavaScript

"use strict";(globalThis.webpackChunkdocs_split_developer=globalThis.webpackChunkdocs_split_developer||[]).push([[5475],{8517:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>c,contentTitle:()=>a,default:()=>h,frontMatter:()=>t,metadata:()=>s,toc:()=>d});const s=JSON.parse('{"id":"period-calculation-theory","title":"Period Calculation Theory","description":"Overview","source":"@site/docs/period-calculation-theory.md","sourceDirName":".","slug":"/period-calculation-theory","permalink":"/hass.tibber_prices/developer/period-calculation-theory","draft":false,"unlisted":false,"editUrl":"https://github.com/jpawlowski/hass.tibber_prices/tree/main/docs/developer/docs/period-calculation-theory.md","tags":[],"version":"current","lastUpdatedAt":1764985026000,"frontMatter":{},"sidebar":"tutorialSidebar","previous":{"title":"Debugging Guide","permalink":"/hass.tibber_prices/developer/debugging"},"next":{"title":"Refactoring Guide","permalink":"/hass.tibber_prices/developer/refactoring-guide"}}');var r=i(4848),l=i(8453);const t={},a="Period Calculation Theory",c={},d=[{value:"Overview",id:"overview",level:2},{value:"Core Filtering Criteria",id:"core-filtering-criteria",level:2},{value:"1. Flex Filter (Price Distance from Reference)",id:"1-flex-filter-price-distance-from-reference",level:3},{value:"2. Min Distance Filter (Distance from Daily Average)",id:"2-min-distance-filter-distance-from-daily-average",level:3},{value:"3. Level Filter (Price Level Classification)",id:"3-level-filter-price-level-classification",level:3},{value:"The Flex \xd7 Min_Distance Conflict",id:"the-flex--min_distance-conflict",level:2},{value:"Problem Statement",id:"problem-statement",level:3},{value:"Scenario: Best Price with Flex=50%, Min_Distance=5%",id:"scenario-best-price-with-flex50-min_distance5",level:4},{value:"Mathematical Analysis",id:"mathematical-analysis",level:3},{value:"Solution: Dynamic Min_Distance Scaling",id:"solution-dynamic-min_distance-scaling",level:3},{value:"Flex Limits and Safety Caps",id:"flex-limits-and-safety-caps",level:2},{value:"Implementation Constants",id:"implementation-constants",level:3},{value:"Rationale for Asymmetric Defaults",id:"rationale-for-asymmetric-defaults",level:3},{value:"Best Price: Optimization Focus",id:"best-price-optimization-focus",level:4},{value:"Peak Price: Warning Focus",id:"peak-price-warning-focus",level:4},{value:"Mathematical Justification",id:"mathematical-justification",level:4},{value:"Design Alternatives Considered",id:"design-alternatives-considered",level:4},{value:"Flex Limits and Safety Caps",id:"flex-limits-and-safety-caps-1",level:2},{value:"1. Absolute Maximum: 50% (MAX_SAFE_FLEX)",id:"1-absolute-maximum-50-max_safe_flex",level:4},{value:"2. Outlier Filtering Maximum: 25%",id:"2-outlier-filtering-maximum-25",level:4},{value:"Recommended Ranges (User Guidance)",id:"recommended-ranges-user-guidance",level:3},{value:"With Relaxation Enabled (Recommended)",id:"with-relaxation-enabled-recommended",level:4},{value:"Without Relaxation",id:"without-relaxation",level:4},{value:"Relaxation Strategy",id:"relaxation-strategy",level:2},{value:"Purpose",id:"purpose",level:3},{value:"Multi-Phase Approach",id:"multi-phase-approach",level:3},{value:"Relaxation Increments",id:"relaxation-increments",level:3},{value:"Filter Combination Strategy",id:"filter-combination-strategy",level:3},{value:"Implementation Notes",id:"implementation-notes",level:2},{value:"Key Files and Functions",id:"key-files-and-functions",level:3},{value:"Outlier Filtering Implementation",id:"outlier-filtering-implementation",level:4},{value:"Debugging Tips",id:"debugging-tips",level:2},{value:"Common Configuration Pitfalls",id:"common-configuration-pitfalls",level:2},{value:"\u274c Anti-Pattern 1: High Flex with Relaxation",id:"-anti-pattern-1-high-flex-with-relaxation",level:3},{value:"\u274c Anti-Pattern 2: Zero Min_Distance",id:"-anti-pattern-2-zero-min_distance",level:3},{value:"\u274c Anti-Pattern 3: Conflicting Flex + Distance",id:"-anti-pattern-3-conflicting-flex--distance",level:3},{value:"Testing Scenarios",id:"testing-scenarios",level:2},{value:"Scenario 1: Normal Day (Good Variation)",id:"scenario-1-normal-day-good-variation",level:3},{value:"Scenario 2: Flat Day (Poor Variation)",id:"scenario-2-flat-day-poor-variation",level:3},{value:"Scenario 3: Extreme Day (High Volatility)",id:"scenario-3-extreme-day-high-volatility",level:3},{value:"Scenario 4: Relaxation Success",id:"scenario-4-relaxation-success",level:3},{value:"Scenario 5: Relaxation Exhausted",id:"scenario-5-relaxation-exhausted",level:3},{value:"Debugging Checklist",id:"debugging-checklist",level:3},{value:"Future Enhancements",id:"future-enhancements",level:2},{value:"Potential Improvements",id:"potential-improvements",level:3},{value:"Known Limitations",id:"known-limitations",level:3},{value:"Future Enhancements",id:"future-enhancements-1",level:2},{value:"Potential Improvements",id:"potential-improvements-1",level:3},{value:"1. Adaptive Flex Calculation (Not Yet Implemented)",id:"1-adaptive-flex-calculation-not-yet-implemented",level:4},{value:"2. Machine Learning Approach (Future Work)",id:"2-machine-learning-approach-future-work",level:4},{value:"3. Multi-Objective Optimization (Research Idea)",id:"3-multi-objective-optimization-research-idea",level:4},{value:"Known Limitations",id:"known-limitations-1",level:3},{value:"1. Fixed Increment Step",id:"1-fixed-increment-step",level:4},{value:"2. Linear Distance Scaling",id:"2-linear-distance-scaling",level:4},{value:"3. No Temporal Distribution Consideration",id:"3-no-temporal-distribution-consideration",level:4},{value:"4. Period Boundary Handling",id:"4-period-boundary-handling",level:4},{value:"References",id:"references",level:2},{value:"Changelog",id:"changelog",level:2}];function o(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",h4:"h4",header:"header",hr:"hr",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,l.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.header,{children:(0,r.jsx)(n.h1,{id:"period-calculation-theory",children:"Period Calculation Theory"})}),"\n",(0,r.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,r.jsxs)(n.p,{children:["This document explains the mathematical foundations and design decisions behind the period calculation algorithm, particularly focusing on the interaction between ",(0,r.jsx)(n.strong,{children:"Flexibility (Flex)"}),", ",(0,r.jsx)(n.strong,{children:"Minimum Distance from Average"}),", and ",(0,r.jsx)(n.strong,{children:"Relaxation Strategy"}),"."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Target Audience:"})," Developers maintaining or extending the period calculation logic."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Related Files:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"coordinator/period_handlers/core.py"})," - Main calculation entry point"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"coordinator/period_handlers/level_filtering.py"})," - Flex and distance filtering"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"coordinator/period_handlers/relaxation.py"})," - Multi-phase relaxation strategy"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"coordinator/periods.py"})," - Period calculator orchestration"]}),"\n"]}),"\n",(0,r.jsx)(n.hr,{}),"\n",(0,r.jsx)(n.h2,{id:"core-filtering-criteria",children:"Core Filtering Criteria"}),"\n",(0,r.jsxs)(n.p,{children:["Period detection uses ",(0,r.jsx)(n.strong,{children:"three independent filters"})," (all must pass):"]}),"\n",(0,r.jsx)(n.h3,{id:"1-flex-filter-price-distance-from-reference",children:"1. Flex Filter (Price Distance from Reference)"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Purpose:"})," Limit how far prices can deviate from the daily min/max."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Logic:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"# Best Price: Price must be within flex% ABOVE daily minimum\nin_flex = price <= (daily_min + daily_min \xd7 flex)\n\n# Peak Price: Price must be within flex% BELOW daily maximum\nin_flex = price >= (daily_max - daily_max \xd7 flex)\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Example (Best Price):"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Daily Min: 10 ct/kWh"}),"\n",(0,r.jsx)(n.li,{children:"Flex: 15%"}),"\n",(0,r.jsx)(n.li,{children:"Acceptance Range: 0 - 11.5 ct/kWh (10 + 10\xd70.15)"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"2-min-distance-filter-distance-from-daily-average",children:"2. Min Distance Filter (Distance from Daily Average)"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Purpose:"})," Ensure periods are ",(0,r.jsx)(n.strong,{children:"significantly"})," cheaper/more expensive than average, not just marginally better."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Logic:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"# Best Price: Price must be at least min_distance% BELOW daily average\nmeets_distance = price <= (daily_avg \xd7 (1 - min_distance/100))\n\n# Peak Price: Price must be at least min_distance% ABOVE daily average\nmeets_distance = price >= (daily_avg \xd7 (1 + min_distance/100))\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Example (Best Price):"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Daily Avg: 15 ct/kWh"}),"\n",(0,r.jsx)(n.li,{children:"Min Distance: 5%"}),"\n",(0,r.jsx)(n.li,{children:"Acceptance Range: 0 - 14.25 ct/kWh (15 \xd7 0.95)"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"3-level-filter-price-level-classification",children:"3. Level Filter (Price Level Classification)"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Purpose:"})," Restrict periods to specific price classifications (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE)."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Logic:"})," See ",(0,r.jsx)(n.code,{children:"level_filtering.py"})," for gap tolerance details."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Volatility Thresholds - Important Separation:"})}),"\n",(0,r.jsxs)(n.p,{children:["The integration maintains ",(0,r.jsx)(n.strong,{children:"two independent sets"})," of volatility thresholds:"]}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Sensor Thresholds"})," (user-configurable via ",(0,r.jsx)(n.code,{children:"CONF_VOLATILITY_*_THRESHOLD"}),")"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Purpose: Display classification in ",(0,r.jsx)(n.code,{children:"sensor.tibber_home_volatility_*"})]}),"\n",(0,r.jsx)(n.li,{children:"Default: LOW < 10%, MEDIUM < 20%, HIGH \u2265 20%"}),"\n",(0,r.jsx)(n.li,{children:"User can adjust in config flow options"}),"\n",(0,r.jsx)(n.li,{children:"Affects: Sensor state/attributes only"}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Period Filter Thresholds"})," (internal, fixed)"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Purpose: Level filter criteria when using ",(0,r.jsx)(n.code,{children:'level="volatility_low"'})," etc."]}),"\n",(0,r.jsxs)(n.li,{children:["Source: ",(0,r.jsx)(n.code,{children:"PRICE_LEVEL_THRESHOLDS"})," in ",(0,r.jsx)(n.code,{children:"const.py"})]}),"\n",(0,r.jsx)(n.li,{children:"Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH \u2265 20%)"}),"\n",(0,r.jsxs)(n.li,{children:["User ",(0,r.jsx)(n.strong,{children:"cannot"})," adjust these"]}),"\n",(0,r.jsx)(n.li,{children:"Affects: Period candidate selection"}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Rationale for Separation:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Sensor thresholds"}),' = Display preference ("I want to see LOW at 15% instead of 10%")']}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Period thresholds"})," = Algorithm configuration (tested defaults, complex interactions)"]}),"\n",(0,r.jsx)(n.li,{children:"Changing sensor display should not affect automation behavior"}),"\n",(0,r.jsx)(n.li,{children:"Prevents unexpected side effects when user adjusts sensor classification"}),"\n",(0,r.jsx)(n.li,{children:"Period calculation has many interacting filters (Flex, Distance, Level) - exposing all internals would be error-prone"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Implementation:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:'# Sensor classification uses user config\nuser_low_threshold = config_entry.options.get(CONF_VOLATILITY_LOW_THRESHOLD, 10)\n\n# Period filter uses fixed constants\nperiod_low_threshold = PRICE_LEVEL_THRESHOLDS["volatility_low"] # Always 10%\n'})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Status:"})," Intentional design decision (Nov 2025). No plans to expose period thresholds to users."]}),"\n",(0,r.jsx)(n.hr,{}),"\n",(0,r.jsx)(n.h2,{id:"the-flex--min_distance-conflict",children:"The Flex \xd7 Min_Distance Conflict"}),"\n",(0,r.jsx)(n.h3,{id:"problem-statement",children:"Problem Statement"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"These two filters can conflict when Flex is high!"})}),"\n",(0,r.jsx)(n.h4,{id:"scenario-best-price-with-flex50-min_distance5",children:"Scenario: Best Price with Flex=50%, Min_Distance=5%"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Given:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Daily Min: 10 ct/kWh"}),"\n",(0,r.jsx)(n.li,{children:"Daily Avg: 15 ct/kWh"}),"\n",(0,r.jsx)(n.li,{children:"Daily Max: 20 ct/kWh"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Flex Filter (50%):"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"Max accepted = 10 + (10 \xd7 0.50) = 15 ct/kWh\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Min Distance Filter (5%):"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"Max accepted = 15 \xd7 (1 - 0.05) = 14.25 ct/kWh\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Conflict:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Interval at 14.8 ct/kWh:","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"\u2705 Flex: 14.8 \u2264 15 (PASS)"}),"\n",(0,r.jsx)(n.li,{children:"\u274c Distance: 14.8 > 14.25 (FAIL)"}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Result:"})," Rejected by Min_Distance even though Flex allows it!"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"The Issue:"})," At high Flex values, Min_Distance becomes the dominant filter and blocks intervals that Flex would permit. This defeats the purpose of having high Flex."]}),"\n",(0,r.jsx)(n.h3,{id:"mathematical-analysis",children:"Mathematical Analysis"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Conflict condition for Best Price:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"daily_min \xd7 (1 + flex) > daily_avg \xd7 (1 - min_distance/100)\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Typical values:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Min = 10, Avg = 15, Min_Distance = 5%"}),"\n",(0,r.jsxs)(n.li,{children:["Conflict occurs when: ",(0,r.jsx)(n.code,{children:"10 \xd7 (1 + flex) > 14.25"})]}),"\n",(0,r.jsxs)(n.li,{children:["Simplify: ",(0,r.jsx)(n.code,{children:"flex > 0.425"})," (42.5%)"]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Below 42.5% Flex:"})," Both filters contribute meaningfully.\n",(0,r.jsx)(n.strong,{children:"Above 42.5% Flex:"})," Min_Distance dominates and blocks intervals."]}),"\n",(0,r.jsx)(n.h3,{id:"solution-dynamic-min_distance-scaling",children:"Solution: Dynamic Min_Distance Scaling"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Approach:"})," Reduce Min_Distance proportionally as Flex increases."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Formula:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"if flex > 0.20: # 20% threshold\n flex_excess = flex - 0.20\n scale_factor = max(0.25, 1.0 - (flex_excess \xd7 2.5))\n adjusted_min_distance = original_min_distance \xd7 scale_factor\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Scaling Table (Original Min_Distance = 5%):"})}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Flex"}),(0,r.jsx)(n.th,{children:"Scale Factor"}),(0,r.jsx)(n.th,{children:"Adjusted Min_Distance"}),(0,r.jsx)(n.th,{children:"Rationale"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"\u226420%"}),(0,r.jsx)(n.td,{children:"1.00"}),(0,r.jsx)(n.td,{children:"5.0%"}),(0,r.jsx)(n.td,{children:"Standard - both filters relevant"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"25%"}),(0,r.jsx)(n.td,{children:"0.88"}),(0,r.jsx)(n.td,{children:"4.4%"}),(0,r.jsx)(n.td,{children:"Slight reduction"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"30%"}),(0,r.jsx)(n.td,{children:"0.75"}),(0,r.jsx)(n.td,{children:"3.75%"}),(0,r.jsx)(n.td,{children:"Moderate reduction"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"40%"}),(0,r.jsx)(n.td,{children:"0.50"}),(0,r.jsx)(n.td,{children:"2.5%"}),(0,r.jsx)(n.td,{children:"Strong reduction - Flex dominates"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"50%"}),(0,r.jsx)(n.td,{children:"0.25"}),(0,r.jsx)(n.td,{children:"1.25%"}),(0,r.jsx)(n.td,{children:"Minimal distance - Flex decides"})]})]})]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Why stop at 25% of original?"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Min_Distance ensures periods are ",(0,r.jsx)(n.strong,{children:"significantly"})," different from average"]}),"\n",(0,r.jsx)(n.li,{children:'Even at 1.25%, prevents "flat days" (little price variation) from accepting every interval'}),"\n",(0,r.jsx)(n.li,{children:'Maintains semantic meaning: "this is a meaningful best/peak price period"'}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Implementation:"})," See ",(0,r.jsx)(n.code,{children:"level_filtering.py"})," \u2192 ",(0,r.jsx)(n.code,{children:"check_interval_criteria()"})]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Code Extract:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:'# coordinator/period_handlers/level_filtering.py\n\nFLEX_SCALING_THRESHOLD = 0.20 # 20% - start adjusting min_distance\nSCALE_FACTOR_WARNING_THRESHOLD = 0.8 # Log when reduction > 20%\n\ndef check_interval_criteria(price, criteria):\n # ... flex check ...\n\n # Dynamic min_distance scaling\n adjusted_min_distance = criteria.min_distance_from_avg\n flex_abs = abs(criteria.flex)\n\n if flex_abs > FLEX_SCALING_THRESHOLD:\n flex_excess = flex_abs - 0.20 # How much above 20%\n scale_factor = max(0.25, 1.0 - (flex_excess \xd7 2.5))\n adjusted_min_distance = criteria.min_distance_from_avg \xd7 scale_factor\n\n if scale_factor < SCALE_FACTOR_WARNING_THRESHOLD:\n _LOGGER.debug(\n "High flex %.1f%% detected: Reducing min_distance %.1f%% \u2192 %.1f%%",\n flex_abs \xd7 100,\n criteria.min_distance_from_avg,\n adjusted_min_distance,\n )\n\n # Apply adjusted min_distance in distance check\n meets_min_distance = (\n price <= avg_price \xd7 (1 - adjusted_min_distance/100) # Best Price\n # OR\n price >= avg_price \xd7 (1 + adjusted_min_distance/100) # Peak Price\n )\n'})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Why Linear Scaling?"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Simple and predictable"}),"\n",(0,r.jsx)(n.li,{children:"No abrupt behavior changes"}),"\n",(0,r.jsx)(n.li,{children:"Easy to reason about for users and developers"}),"\n",(0,r.jsx)(n.li,{children:"Alternative considered: Exponential scaling (rejected as too aggressive)"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Why 25% Minimum?"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Below this, min_distance loses semantic meaning"}),"\n",(0,r.jsx)(n.li,{children:"Even on flat days, some quality filter needed"}),"\n",(0,r.jsx)(n.li,{children:'Prevents "every interval is a period" scenario'}),"\n",(0,r.jsx)(n.li,{children:'Maintains user expectation: "best/peak price means notably different"'}),"\n"]}),"\n",(0,r.jsx)(n.hr,{}),"\n",(0,r.jsx)(n.h2,{id:"flex-limits-and-safety-caps",children:"Flex Limits and Safety Caps"}),"\n",(0,r.jsx)(n.h3,{id:"implementation-constants",children:"Implementation Constants"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsxs)(n.strong,{children:["Defined in ",(0,r.jsx)(n.code,{children:"coordinator/period_handlers/core.py"}),":"]})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"MAX_SAFE_FLEX = 0.50 # 50% - hard cap: above this, period detection becomes unreliable\nMAX_OUTLIER_FLEX = 0.25 # 25% - cap for outlier filtering: above this, spike detection too permissive\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsxs)(n.strong,{children:["Defined in ",(0,r.jsx)(n.code,{children:"const.py"}),":"]})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"DEFAULT_BEST_PRICE_FLEX = 15 # 15% base - optimal for relaxation mode (default enabled)\nDEFAULT_PEAK_PRICE_FLEX = -20 # 20% base (negative for peak detection)\nDEFAULT_RELAXATION_ATTEMPTS_BEST = 11 # 11 steps: 15% \u2192 48% (3% increment per step)\nDEFAULT_RELAXATION_ATTEMPTS_PEAK = 11 # 11 steps: 20% \u2192 50% (3% increment per step)\nDEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH = 60 # 60 minutes\nDEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH = 30 # 30 minutes\nDEFAULT_BEST_PRICE_MIN_DISTANCE_FROM_AVG = 5 # 5% minimum distance\nDEFAULT_PEAK_PRICE_MIN_DISTANCE_FROM_AVG = 5 # 5% minimum distance\n"})}),"\n",(0,r.jsx)(n.h3,{id:"rationale-for-asymmetric-defaults",children:"Rationale for Asymmetric Defaults"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Why Best Price \u2260 Peak Price?"})}),"\n",(0,r.jsx)(n.p,{children:"The different defaults reflect fundamentally different use cases:"}),"\n",(0,r.jsx)(n.h4,{id:"best-price-optimization-focus",children:"Best Price: Optimization Focus"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Goal:"})," Find practical time windows for running appliances"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Constraints:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Appliances need time to complete cycles (dishwasher: 2-3h, EV charging: 4-8h)"}),"\n",(0,r.jsx)(n.li,{children:"Short periods are impractical (not worth automation overhead)"}),"\n",(0,r.jsx)(n.li,{children:'User wants genuinely cheap times, not just "slightly below average"'}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Defaults:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"60 min minimum"})," - Ensures period is long enough for meaningful use"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"15% flex"})," - Stricter selection, focuses on truly cheap times"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Reasoning:"})," Better to find fewer, higher-quality periods than many mediocre ones"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"User behavior:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Automations trigger actions (turn on devices)"}),"\n",(0,r.jsx)(n.li,{children:"Wrong automation = wasted energy/money"}),"\n",(0,r.jsx)(n.li,{children:"Preference: Conservative (miss some savings) over aggressive (false positives)"}),"\n"]}),"\n",(0,r.jsx)(n.h4,{id:"peak-price-warning-focus",children:"Peak Price: Warning Focus"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Goal:"})," Alert users to expensive periods for consumption reduction"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Constraints:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Brief price spikes still matter (even 15-30 min is worth avoiding)"}),"\n",(0,r.jsx)(n.li,{children:"Early warning more valuable than perfect accuracy"}),"\n",(0,r.jsx)(n.li,{children:"User can manually decide whether to react"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Defaults:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"30 min minimum"})," - Catches shorter expensive spikes"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"20% flex"})," - More permissive, earlier detection"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Reasoning:"})," Better to warn early (even if not peak) than miss expensive periods"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"User behavior:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Notifications/alerts (informational)"}),"\n",(0,r.jsx)(n.li,{children:"Wrong alert = minor inconvenience, not cost"}),"\n",(0,r.jsx)(n.li,{children:"Preference: Sensitive (catch more) over specific (catch only extremes)"}),"\n"]}),"\n",(0,r.jsx)(n.h4,{id:"mathematical-justification",children:"Mathematical Justification"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Peak Price Volatility:"})}),"\n",(0,r.jsx)(n.p,{children:"Price curves tend to have:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Sharp spikes"})," during peak hours (morning/evening)"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Shorter duration"})," at maximum (1-2 hours typical)"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Higher variance"})," in peak times than cheap times"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Example day:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"Cheap period: 02:00-07:00 (5 hours at 10-12 ct) \u2190 Gradual, stable\nExpensive period: 17:00-18:30 (1.5 hours at 35-40 ct) \u2190 Sharp, brief\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Implication:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Stricter flex on peak (15%) might miss real expensive periods (too brief)"}),"\n",(0,r.jsx)(n.li,{children:"Longer min_length (60 min) might exclude legitimate spikes"}),"\n",(0,r.jsx)(n.li,{children:"Solution: More flexible thresholds for peak detection"}),"\n"]}),"\n",(0,r.jsx)(n.h4,{id:"design-alternatives-considered",children:"Design Alternatives Considered"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Option 1: Symmetric defaults (rejected)"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Both 60 min, both 15% flex"}),"\n",(0,r.jsx)(n.li,{children:"Problem: Misses short but expensive spikes"}),"\n",(0,r.jsx)(n.li,{children:'User feedback: "Why didn\'t I get warned about the 30-min price spike?"'}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Option 2: Same defaults, let users figure it out (rejected)"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"No guidance on best practices"}),"\n",(0,r.jsx)(n.li,{children:"Users would need to experiment to find good values"}),"\n",(0,r.jsx)(n.li,{children:"Most users stick with defaults, so defaults matter"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Option 3: Current approach (adopted)"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"All values user-configurable"})," via config flow options"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Different installation defaults"})," for Best Price vs. Peak Price"]}),"\n",(0,r.jsx)(n.li,{children:"Defaults reflect recommended practices for each use case"}),"\n",(0,r.jsx)(n.li,{children:"Users who need different behavior can adjust"}),"\n",(0,r.jsx)(n.li,{children:"Most users benefit from sensible defaults without configuration"}),"\n"]}),"\n",(0,r.jsx)(n.hr,{}),"\n",(0,r.jsx)(n.h2,{id:"flex-limits-and-safety-caps-1",children:"Flex Limits and Safety Caps"}),"\n",(0,r.jsx)(n.h4,{id:"1-absolute-maximum-50-max_safe_flex",children:"1. Absolute Maximum: 50% (MAX_SAFE_FLEX)"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Enforcement:"})," ",(0,r.jsx)(n.code,{children:"core.py"})," caps ",(0,r.jsx)(n.code,{children:"abs(flex)"})," at 0.50 (50%)"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Rationale:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Above 50%, period detection becomes unreliable"}),"\n",(0,r.jsx)(n.li,{children:"Best Price: Almost entire day qualifies (Min + 50% typically covers 60-80% of intervals)"}),"\n",(0,r.jsx)(n.li,{children:"Peak Price: Similar issue with Max - 50%"}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Result:"})," Either massive periods (entire day) or no periods (min_length not met)"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Warning Message:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"Flex XX% exceeds maximum safe value! Capping at 50%.\nRecommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation.\n"})}),"\n",(0,r.jsx)(n.h4,{id:"2-outlier-filtering-maximum-25",children:"2. Outlier Filtering Maximum: 25%"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Enforcement:"})," ",(0,r.jsx)(n.code,{children:"core.py"})," caps outlier filtering flex at 0.25 (25%)"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Rationale:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:'Outlier filtering uses Flex to determine "stable context" threshold'}),"\n",(0,r.jsx)(n.li,{children:'At > 25% Flex, almost any price swing is considered "stable"'}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Result:"})," Legitimate price shifts aren't smoothed, breaking period formation"]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Note:"})," User's Flex still applies to period criteria (",(0,r.jsx)(n.code,{children:"in_flex"})," check), only outlier filtering is capped."]}),"\n",(0,r.jsx)(n.h3,{id:"recommended-ranges-user-guidance",children:"Recommended Ranges (User Guidance)"}),"\n",(0,r.jsx)(n.h4,{id:"with-relaxation-enabled-recommended",children:"With Relaxation Enabled (Recommended)"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Optimal:"})," 10-20%"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Relaxation increases Flex incrementally: 15% \u2192 18% \u2192 21% \u2192 ..."}),"\n",(0,r.jsx)(n.li,{children:"Low baseline ensures relaxation has room to work"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Warning Threshold:"})," > 25%"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:'INFO log: "Base flex is on the high side"'}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"High Warning:"})," > 30%"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:'WARNING log: "Base flex is very high for relaxation mode!"'}),"\n",(0,r.jsx)(n.li,{children:"Recommendation: Lower to 15-20%"}),"\n"]}),"\n",(0,r.jsx)(n.h4,{id:"without-relaxation",children:"Without Relaxation"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Optimal:"})," 20-35%"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"No automatic adjustment, must be sufficient from start"}),"\n",(0,r.jsx)(n.li,{children:"Higher baseline acceptable since no relaxation fallback"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Maximum Useful:"})," ~50%"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Above this, period detection degrades (see Hard Limits)"}),"\n"]}),"\n",(0,r.jsx)(n.hr,{}),"\n",(0,r.jsx)(n.h2,{id:"relaxation-strategy",children:"Relaxation Strategy"}),"\n",(0,r.jsx)(n.h3,{id:"purpose",children:"Purpose"}),"\n",(0,r.jsxs)(n.p,{children:["Ensure ",(0,r.jsx)(n.strong,{children:"minimum periods per day"})," are found even when baseline filters are too strict."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Use Case:"})," User configures strict filters (low Flex, restrictive Level) but wants guarantee of N periods/day for automation reliability."]}),"\n",(0,r.jsx)(n.h3,{id:"multi-phase-approach",children:"Multi-Phase Approach"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Each day processed independently:"})}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsx)(n.li,{children:"Calculate baseline periods with user's config"}),"\n",(0,r.jsx)(n.li,{children:"If insufficient periods found, enter relaxation loop"}),"\n",(0,r.jsx)(n.li,{children:"Try progressively relaxed filter combinations"}),"\n",(0,r.jsx)(n.li,{children:"Stop when target reached or all attempts exhausted"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"relaxation-increments",children:"Relaxation Increments"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Current Implementation (November 2025):"})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"File:"})," ",(0,r.jsx)(n.code,{children:"coordinator/period_handlers/relaxation.py"})]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"# Hard-coded 3% increment per step (reliability over configurability)\nflex_increment = 0.03 # 3% per step\nbase_flex = abs(config.flex)\n\n# Generate flex levels\nfor attempt in range(max_relaxation_attempts):\n flex_level = base_flex + (attempt \xd7 flex_increment)\n # Try flex_level with both filter combinations\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Constants:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"FLEX_WARNING_THRESHOLD_RELAXATION = 0.25 # 25% - INFO: suggest lowering to 15-20%\nFLEX_HIGH_THRESHOLD_RELAXATION = 0.30 # 30% - WARNING: very high for relaxation mode\nMAX_FLEX_HARD_LIMIT = 0.50 # 50% - absolute maximum (enforced in core.py)\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Design Decisions:"})}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Why 3% fixed increment?"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Predictable escalation path (15% \u2192 18% \u2192 21% \u2192 ...)"}),"\n",(0,r.jsx)(n.li,{children:"Independent of base flex (works consistently)"}),"\n",(0,r.jsx)(n.li,{children:"11 attempts covers full useful range (15% \u2192 48%)"}),"\n",(0,r.jsx)(n.li,{children:"Balance: Not too slow (2%), not too fast (5%)"}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Why hard-coded, not configurable?"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Prevents user misconfiguration"}),"\n",(0,r.jsx)(n.li,{children:"Simplifies mental model (fewer knobs to turn)"}),"\n",(0,r.jsx)(n.li,{children:"Reliable behavior across all configurations"}),"\n",(0,r.jsxs)(n.li,{children:["If needed, user adjusts ",(0,r.jsx)(n.code,{children:"max_relaxation_attempts"})," (fewer/more steps)"]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Why warn at 25% base flex?"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"At 25% base, first relaxation step reaches 28%"}),"\n",(0,r.jsx)(n.li,{children:"Above 30%, entering diminishing returns territory"}),"\n",(0,r.jsx)(n.li,{children:"User likely doesn't need relaxation with such high base flex"}),"\n",(0,r.jsx)(n.li,{children:"Should either: (a) lower base flex, or (b) disable relaxation"}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Historical Context (Pre-November 2025):"})}),"\n",(0,r.jsx)(n.p,{children:"The algorithm previously used percentage-based increments that scaled with base flex:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"increment = base_flex \xd7 (step_pct / 100) # REMOVED\n"})}),"\n",(0,r.jsx)(n.p,{children:"This caused exponential escalation with high base flex values (e.g., 40% \u2192 50% \u2192 60% \u2192 70% in just 6 steps), making behavior unpredictable. The fixed 3% increment solves this by providing consistent, controlled escalation regardless of starting point."}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Warning Messages:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:'if base_flex >= FLEX_HIGH_THRESHOLD_RELAXATION: # 30%\n _LOGGER.warning(\n "Base flex %.1f%% is very high for relaxation mode! "\n "Consider lowering to 15-20%% or disabling relaxation.",\n base_flex \xd7 100,\n )\nelif base_flex >= FLEX_WARNING_THRESHOLD_RELAXATION: # 25%\n _LOGGER.info(\n "Base flex %.1f%% is on the high side. "\n "Consider 15-20%% for optimal relaxation effectiveness.",\n base_flex \xd7 100,\n )\n'})}),"\n",(0,r.jsx)(n.h3,{id:"filter-combination-strategy",children:"Filter Combination Strategy"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Per Flex level, try in order:"})}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsx)(n.li,{children:"Original Level filter"}),"\n",(0,r.jsx)(n.li,{children:'Level filter = "any" (disabled)'}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Early Exit:"})," Stop immediately when target reached (don't try unnecessary combinations)"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Example Flow (target=2 periods/day):"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"Day 2025-11-19:\n1. Baseline flex=15%: Found 1 period (need 2)\n2. Flex=18% + level=cheap: Found 1 period\n3. Flex=18% + level=any: Found 2 periods \u2192 SUCCESS (stop)\n"})}),"\n",(0,r.jsx)(n.hr,{}),"\n",(0,r.jsx)(n.h2,{id:"implementation-notes",children:"Implementation Notes"}),"\n",(0,r.jsx)(n.h3,{id:"key-files-and-functions",children:"Key Files and Functions"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Period Calculation Entry Point:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"# coordinator/period_handlers/core.py\ndef calculate_periods(\n all_prices: list[dict],\n config: PeriodConfig,\n time: TimeService,\n) -> dict[str, Any]\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Flex + Distance Filtering:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"# coordinator/period_handlers/level_filtering.py\ndef check_interval_criteria(\n price: float,\n criteria: IntervalCriteria,\n) -> tuple[bool, bool] # (in_flex, meets_min_distance)\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Relaxation Orchestration:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"# coordinator/period_handlers/relaxation.py\ndef calculate_periods_with_relaxation(...) -> tuple[dict, dict]\ndef relax_single_day(...) -> tuple[dict, dict]\n"})}),"\n",(0,r.jsx)(n.h4,{id:"outlier-filtering-implementation",children:"Outlier Filtering Implementation"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"File:"})," ",(0,r.jsx)(n.code,{children:"coordinator/period_handlers/outlier_filtering.py"})]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Purpose:"})," Detect and smooth isolated price spikes before period identification to prevent artificial fragmentation."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Algorithm Details:"})}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Linear Regression Prediction:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Uses surrounding intervals to predict expected price"}),"\n",(0,r.jsx)(n.li,{children:"Window size: 3+ intervals (MIN_CONTEXT_SIZE)"}),"\n",(0,r.jsx)(n.li,{children:"Calculates trend slope and standard deviation"}),"\n",(0,r.jsxs)(n.li,{children:["Formula: ",(0,r.jsx)(n.code,{children:"predicted = mean + slope \xd7 (position - center)"})]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Confidence Intervals:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"95% confidence level (2 standard deviations)"}),"\n",(0,r.jsx)(n.li,{children:"Tolerance = 2.0 \xd7 std_dev (CONFIDENCE_LEVEL constant)"}),"\n",(0,r.jsxs)(n.li,{children:["Outlier if: ",(0,r.jsx)(n.code,{children:"|actual - predicted| > tolerance"})]}),"\n",(0,r.jsx)(n.li,{children:"Accounts for natural price volatility in context window"}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Symmetry Check:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Rejects asymmetric outliers (threshold: 1.5 std dev)"}),"\n",(0,r.jsx)(n.li,{children:"Preserves legitimate price shifts (morning/evening peaks)"}),"\n",(0,r.jsxs)(n.li,{children:["Algorithm:","\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"residual = abs(actual - predicted)\nsymmetry_threshold = 1.5 \xd7 std_dev\n\nif residual > tolerance:\n # Check if spike is symmetric in context\n context_residuals = [abs(p - pred) for p, pred in context]\n avg_context_residual = mean(context_residuals)\n\n if residual > symmetry_threshold \xd7 avg_context_residual:\n # Asymmetric spike \u2192 smooth it\n else:\n # Symmetric (part of trend) \u2192 keep it\n"})}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Enhanced Zigzag Detection:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Detects spike clusters via relative volatility"}),"\n",(0,r.jsx)(n.li,{children:"Threshold: 2.0\xd7 local volatility (RELATIVE_VOLATILITY_THRESHOLD)"}),"\n",(0,r.jsx)(n.li,{children:"Single-pass algorithm (no iteration needed)"}),"\n",(0,r.jsx)(n.li,{children:"Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes)"}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Constants:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"# coordinator/period_handlers/outlier_filtering.py\n\nCONFIDENCE_LEVEL = 2.0 # 95% confidence (2 std deviations)\nSYMMETRY_THRESHOLD = 1.5 # Asymmetry detection threshold\nRELATIVE_VOLATILITY_THRESHOLD = 2.0 # Zigzag spike detection\nMIN_CONTEXT_SIZE = 3 # Minimum intervals for regression\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Data Integrity:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Original prices stored in ",(0,r.jsx)(n.code,{children:"_original_price"})," field"]}),"\n",(0,r.jsx)(n.li,{children:"All statistics (daily min/max/avg) use original prices"}),"\n",(0,r.jsx)(n.li,{children:"Smoothing only affects period formation logic"}),"\n",(0,r.jsx)(n.li,{children:"Smart counting: Only counts smoothing that changed period outcome"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Performance:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Single pass through price data"}),"\n",(0,r.jsx)(n.li,{children:"O(n) complexity with small context window"}),"\n",(0,r.jsx)(n.li,{children:"No iterative refinement needed"}),"\n",(0,r.jsxs)(n.li,{children:["Typical processing time: ",(0,r.jsx)(n.code,{children:"<"}),"1ms for 96 intervals"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Example Debug Output:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"DEBUG: [2025-11-11T14:30:00+01:00] Outlier detected: 35.2 ct\nDEBUG: Context: 18.5, 19.1, 19.3, 19.8, 20.2 ct\nDEBUG: Residual: 14.5 ct > tolerance: 4.8 ct (2\xd72.4 std dev)\nDEBUG: Trend slope: 0.3 ct/interval (gradual increase)\nDEBUG: Predicted: 20.7 ct (linear regression)\nDEBUG: Smoothed to: 20.7 ct\nDEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) \u2192 confirmed outlier\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Why This Approach?"})}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Linear regression over moving average:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Accounts for price trends (morning ramp-up, evening decline)"}),"\n",(0,r.jsx)(n.li,{children:"Moving average can't predict direction, only level"}),"\n",(0,r.jsx)(n.li,{children:"Better accuracy on non-stationary price curves"}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Symmetry check over fixed threshold:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Prevents false positives on legitimate price shifts"}),"\n",(0,r.jsx)(n.li,{children:"Adapts to local volatility patterns"}),"\n",(0,r.jsx)(n.li,{children:'Preserves user expectation: "expensive during peak hours"'}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Single-pass over iterative:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Predictable behavior (no convergence issues)"}),"\n",(0,r.jsx)(n.li,{children:"Fast and deterministic"}),"\n",(0,r.jsx)(n.li,{children:"Easier to debug and reason about"}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Alternative Approaches Considered:"})}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Median filtering"})," - Rejected: Too aggressive, removes legitimate peaks"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Moving average"})," - Rejected: Can't handle trends"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"IQR (Interquartile Range)"})," - Rejected: Assumes normal distribution"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"RANSAC"})," - Rejected: Overkill for 1D data, slow"]}),"\n"]}),"\n",(0,r.jsx)(n.hr,{}),"\n",(0,r.jsx)(n.h2,{id:"debugging-tips",children:"Debugging Tips"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Enable DEBUG logging:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"# configuration.yaml\nlogger:\n default: info\n logs:\n custom_components.tibber_prices.coordinator.period_handlers: debug\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Key log messages to watch:"})}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:'"Filter statistics: X intervals checked"'})," - Shows how many intervals filtered by each criterion"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:'"After build_periods: X raw periods found"'})," - Periods before min_length filtering"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:'"Day X: Success with flex=Y%"'})," - Relaxation succeeded"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:'"High flex X% detected: Reducing min_distance Y% \u2192 Z%"'})," - Distance scaling active"]}),"\n"]}),"\n",(0,r.jsx)(n.hr,{}),"\n",(0,r.jsx)(n.h2,{id:"common-configuration-pitfalls",children:"Common Configuration Pitfalls"}),"\n",(0,r.jsx)(n.h3,{id:"-anti-pattern-1-high-flex-with-relaxation",children:"\u274c Anti-Pattern 1: High Flex with Relaxation"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Configuration:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"best_price_flex: 40\nenable_relaxation_best: true\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Problem:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Base Flex 40% already very permissive"}),"\n",(0,r.jsx)(n.li,{children:"Relaxation increments further (43%, 46%, 49%, ...)"}),"\n",(0,r.jsx)(n.li,{children:"Quickly approaches 50% cap with diminishing returns"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Solution:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"best_price_flex: 15 # Let relaxation increase it\nenable_relaxation_best: true\n"})}),"\n",(0,r.jsx)(n.h3,{id:"-anti-pattern-2-zero-min_distance",children:"\u274c Anti-Pattern 2: Zero Min_Distance"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Configuration:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"best_price_min_distance_from_avg: 0\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Problem:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:'"Flat days" (little price variation) accept all intervals'}),"\n",(0,r.jsx)(n.li,{children:'Periods lose semantic meaning ("significantly cheap")'}),"\n",(0,r.jsx)(n.li,{children:"May create periods during barely-below-average times"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Solution:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"best_price_min_distance_from_avg: 5 # Use default 5%\n"})}),"\n",(0,r.jsx)(n.h3,{id:"-anti-pattern-3-conflicting-flex--distance",children:"\u274c Anti-Pattern 3: Conflicting Flex + Distance"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Configuration:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"best_price_flex: 45\nbest_price_min_distance_from_avg: 10\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Problem:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Distance filter dominates, making Flex irrelevant"}),"\n",(0,r.jsx)(n.li,{children:"Dynamic scaling helps but still suboptimal"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Solution:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"best_price_flex: 20\nbest_price_min_distance_from_avg: 5\n"})}),"\n",(0,r.jsx)(n.hr,{}),"\n",(0,r.jsx)(n.h2,{id:"testing-scenarios",children:"Testing Scenarios"}),"\n",(0,r.jsx)(n.h3,{id:"scenario-1-normal-day-good-variation",children:"Scenario 1: Normal Day (Good Variation)"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Price Range:"})," 10 - 20 ct/kWh (100% variation)\n",(0,r.jsx)(n.strong,{children:"Average:"})," 15 ct/kWh"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Expected Behavior:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Flex 15%: Should find 2-4 clear best price periods"}),"\n",(0,r.jsx)(n.li,{children:"Flex 30%: Should find 4-8 periods (more lenient)"}),"\n",(0,r.jsx)(n.li,{children:"Min_Distance 5%: Effective throughout range"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Debug Checks:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"DEBUG: Filter statistics: 96 intervals checked\nDEBUG: Filtered by FLEX: 12/96 (12.5%) \u2190 Low percentage = good variation\nDEBUG: Filtered by MIN_DISTANCE: 8/96 (8.3%) \u2190 Both filters active\nDEBUG: After build_periods: 3 raw periods found\n"})}),"\n",(0,r.jsx)(n.h3,{id:"scenario-2-flat-day-poor-variation",children:"Scenario 2: Flat Day (Poor Variation)"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Price Range:"})," 14 - 16 ct/kWh (14% variation)\n",(0,r.jsx)(n.strong,{children:"Average:"})," 15 ct/kWh"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Expected Behavior:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Flex 15%: May find 1-2 small periods (or zero if no clear winners)"}),"\n",(0,r.jsx)(n.li,{children:"Min_Distance 5%: Critical here - ensures only truly cheaper intervals qualify"}),"\n",(0,r.jsx)(n.li,{children:'Without Min_Distance: Would accept almost entire day as "best price"'}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Debug Checks:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"DEBUG: Filter statistics: 96 intervals checked\nDEBUG: Filtered by FLEX: 45/96 (46.9%) \u2190 High percentage = poor variation\nDEBUG: Filtered by MIN_DISTANCE: 52/96 (54.2%) \u2190 Distance filter dominant\nDEBUG: After build_periods: 1 raw period found\nDEBUG: Day 2025-11-11: Baseline insufficient (1 < 2), starting relaxation\n"})}),"\n",(0,r.jsx)(n.h3,{id:"scenario-3-extreme-day-high-volatility",children:"Scenario 3: Extreme Day (High Volatility)"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Price Range:"})," 5 - 40 ct/kWh (700% variation)\n",(0,r.jsx)(n.strong,{children:"Average:"})," 18 ct/kWh"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Expected Behavior:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Flex 15%: Finds multiple very cheap periods (5-6 ct)"}),"\n",(0,r.jsx)(n.li,{children:"Outlier filtering: May smooth isolated spikes (30-40 ct)"}),"\n",(0,r.jsx)(n.li,{children:"Distance filter: Less impactful (clear separation between cheap/expensive)"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Debug Checks:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"DEBUG: Outlier detected: 38.5 ct (threshold: 4.2 ct)\nDEBUG: Smoothed to: 20.1 ct (trend prediction)\nDEBUG: Filter statistics: 96 intervals checked\nDEBUG: Filtered by FLEX: 8/96 (8.3%) \u2190 Very selective\nDEBUG: Filtered by MIN_DISTANCE: 4/96 (4.2%) \u2190 Flex dominates\nDEBUG: After build_periods: 4 raw periods found\n"})}),"\n",(0,r.jsx)(n.h3,{id:"scenario-4-relaxation-success",children:"Scenario 4: Relaxation Success"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Initial State:"})," Baseline finds 1 period, target is 2"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Expected Flow:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0%\nDEBUG: Day 2025-11-11: Baseline found 1 period (need 2)\nDEBUG: Phase 1: flex 18.0% + original filters\nDEBUG: Found 1 period (insufficient)\nDEBUG: Phase 2: flex 18.0% + level=any\nDEBUG: Found 2 periods \u2192 SUCCESS\nINFO: Day 2025-11-11: Success after 1 relaxation phase (2 periods)\n"})}),"\n",(0,r.jsx)(n.h3,{id:"scenario-5-relaxation-exhausted",children:"Scenario 5: Relaxation Exhausted"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Initial State:"})," Strict filters, very flat day"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Expected Flow:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0%\nDEBUG: Day 2025-11-11: Baseline found 0 periods (need 2)\nDEBUG: Phase 1-11: flex 15%\u219248%, all filter combinations tried\nWARNING: Day 2025-11-11: All relaxation phases exhausted, still only 1 period found\nINFO: Period calculation completed: 1/2 days reached target\n"})}),"\n",(0,r.jsx)(n.h3,{id:"debugging-checklist",children:"Debugging Checklist"}),"\n",(0,r.jsx)(n.p,{children:"When debugging period calculation issues:"}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Check Filter Statistics"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Which filter blocks most intervals? (flex, distance, or level)"}),"\n",(0,r.jsx)(n.li,{children:"High flex filtering (>30%) = Need more flexibility or relaxation"}),"\n",(0,r.jsx)(n.li,{children:"High distance filtering (>50%) = Min_distance too strict or flat day"}),"\n",(0,r.jsx)(n.li,{children:"High level filtering = Level filter too restrictive"}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Check Relaxation Behavior"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:'Did relaxation activate? Check for "Baseline insufficient" message'}),"\n",(0,r.jsx)(n.li,{children:"Which phase succeeded? Early success (phase 1-3) = good config"}),"\n",(0,r.jsx)(n.li,{children:"Late success (phase 8-11) = Consider adjusting base config"}),"\n",(0,r.jsx)(n.li,{children:"Exhausted all phases = Unrealistic target for this day's price curve"}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Check Flex Warnings"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"INFO at 25% base flex = On the high side"}),"\n",(0,r.jsx)(n.li,{children:"WARNING at 30% base flex = Too high for relaxation"}),"\n",(0,r.jsx)(n.li,{children:"If seeing these: Lower base flex to 15-20%"}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Check Min_Distance Scaling"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:'Debug messages show "High flex X% detected: Reducing min_distance Y% \u2192 Z%"'}),"\n",(0,r.jsxs)(n.li,{children:["If scale factor ",(0,r.jsx)(n.code,{children:"<"}),"0.8 (20% reduction): High flex is active"]}),"\n",(0,r.jsx)(n.li,{children:"If periods still not found: Filters conflict even with scaling"}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Check Outlier Filtering"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:'Look for "Outlier detected" messages'}),"\n",(0,r.jsxs)(n.li,{children:["Check ",(0,r.jsx)(n.code,{children:"period_interval_smoothed_count"})," attribute"]}),"\n",(0,r.jsx)(n.li,{children:"If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels"}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.hr,{}),"\n",(0,r.jsx)(n.h2,{id:"future-enhancements",children:"Future Enhancements"}),"\n",(0,r.jsx)(n.h3,{id:"potential-improvements",children:"Potential Improvements"}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Adaptive Flex Calculation:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Auto-adjust Flex based on daily price variation"}),"\n",(0,r.jsx)(n.li,{children:"High variation days: Lower Flex needed"}),"\n",(0,r.jsx)(n.li,{children:"Low variation days: Higher Flex needed"}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Machine Learning Approach:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Learn optimal Flex/Distance from user feedback"}),"\n",(0,r.jsx)(n.li,{children:"Classify days by pattern (normal/flat/volatile/bimodal)"}),"\n",(0,r.jsx)(n.li,{children:"Apply pattern-specific defaults"}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Multi-Objective Optimization:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Balance period count vs. quality"}),"\n",(0,r.jsx)(n.li,{children:"Consider period duration vs. price level"}),"\n",(0,r.jsx)(n.li,{children:"Optimize for user's stated use case (EV charging vs. heat pump)"}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"known-limitations",children:"Known Limitations"}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Fixed increment step:"})," 3% cap may be too aggressive for very low base Flex"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Linear distance scaling:"})," Could benefit from non-linear curve"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"No consideration of temporal distribution:"})," May find all periods in one part of day"]}),"\n"]}),"\n",(0,r.jsx)(n.hr,{}),"\n",(0,r.jsx)(n.h2,{id:"future-enhancements-1",children:"Future Enhancements"}),"\n",(0,r.jsx)(n.h3,{id:"potential-improvements-1",children:"Potential Improvements"}),"\n",(0,r.jsx)(n.h4,{id:"1-adaptive-flex-calculation-not-yet-implemented",children:"1. Adaptive Flex Calculation (Not Yet Implemented)"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Concept:"})," Auto-adjust Flex based on daily price variation"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Algorithm:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"# Pseudo-code for adaptive flex\nvariation = (daily_max - daily_min) / daily_avg\n\nif variation < 0.15: # Flat day (< 15% variation)\n adaptive_flex = 0.30 # Need higher flex\nelif variation > 0.50: # High volatility (> 50% variation)\n adaptive_flex = 0.10 # Lower flex sufficient\nelse: # Normal day\n adaptive_flex = 0.15 # Standard flex\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Benefits:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Eliminates need for relaxation on most days"}),"\n",(0,r.jsx)(n.li,{children:"Self-adjusting to market conditions"}),"\n",(0,r.jsx)(n.li,{children:"Better user experience (less configuration needed)"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Challenges:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Harder to predict behavior (less transparent)"}),"\n",(0,r.jsx)(n.li,{children:"May conflict with user's mental model"}),"\n",(0,r.jsx)(n.li,{children:"Needs extensive testing across different markets"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Status:"})," Considered but not implemented (prefer explicit relaxation)"]}),"\n",(0,r.jsx)(n.h4,{id:"2-machine-learning-approach-future-work",children:"2. Machine Learning Approach (Future Work)"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Concept:"})," Learn optimal Flex/Distance from user feedback"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Approach:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Track which periods user actually uses (automation triggers)"}),"\n",(0,r.jsx)(n.li,{children:"Classify days by pattern (normal/flat/volatile/bimodal)"}),"\n",(0,r.jsx)(n.li,{children:"Apply pattern-specific defaults"}),"\n",(0,r.jsx)(n.li,{children:"Learn per-user preferences over time"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Benefits:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Personalized to user's actual behavior"}),"\n",(0,r.jsx)(n.li,{children:"Adapts to local market patterns"}),"\n",(0,r.jsx)(n.li,{children:"Could discover non-obvious patterns"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Challenges:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Requires user feedback mechanism (not implemented)"}),"\n",(0,r.jsx)(n.li,{children:"Privacy concerns (storing usage patterns)"}),"\n",(0,r.jsx)(n.li,{children:'Complexity for users to understand "why this period?"'}),"\n",(0,r.jsx)(n.li,{children:"Cold start problem (new users have no history)"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Status:"})," Theoretical only (no implementation planned)"]}),"\n",(0,r.jsx)(n.h4,{id:"3-multi-objective-optimization-research-idea",children:"3. Multi-Objective Optimization (Research Idea)"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Concept:"})," Balance multiple goals simultaneously"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Goals:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Period count vs. quality (cheap vs. very cheap)"}),"\n",(0,r.jsx)(n.li,{children:"Period duration vs. price level (long mediocre vs. short excellent)"}),"\n",(0,r.jsx)(n.li,{children:"Temporal distribution (spread throughout day vs. clustered)"}),"\n",(0,r.jsx)(n.li,{children:"User's stated use case (EV charging vs. heat pump vs. dishwasher)"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Algorithm:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Pareto optimization (find trade-off frontier)"}),"\n",(0,r.jsx)(n.li,{children:"User chooses point on frontier via preferences"}),"\n",(0,r.jsx)(n.li,{children:"Genetic algorithm or simulated annealing"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Benefits:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"More sophisticated period selection"}),"\n",(0,r.jsx)(n.li,{children:"Better match to user's actual needs"}),"\n",(0,r.jsx)(n.li,{children:"Could handle complex appliance requirements"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Challenges:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Much more complex to implement"}),"\n",(0,r.jsx)(n.li,{children:"Harder to explain to users"}),"\n",(0,r.jsx)(n.li,{children:"Computational cost (may need caching)"}),"\n",(0,r.jsx)(n.li,{children:"Configuration explosion (too many knobs)"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Status:"})," Research idea only (not planned)"]}),"\n",(0,r.jsx)(n.h3,{id:"known-limitations-1",children:"Known Limitations"}),"\n",(0,r.jsx)(n.h4,{id:"1-fixed-increment-step",children:"1. Fixed Increment Step"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Current:"})," 3% cap may be too aggressive for very low base Flex"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Example:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Base flex 5% + 3% increment = 8% (60% increase!)"}),"\n",(0,r.jsx)(n.li,{children:"Base flex 15% + 3% increment = 18% (20% increase)"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Possible Solution:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Percentage-based increment: ",(0,r.jsx)(n.code,{children:"increment = max(base_flex \xd7 0.20, 0.03)"})]}),"\n",(0,r.jsx)(n.li,{children:"This gives: 5% \u2192 6% (20%), 15% \u2192 18% (20%), 40% \u2192 43% (7.5%)"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Why Not Implemented:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Very low base flex (",(0,r.jsx)(n.code,{children:"<"}),"10%) unusual"]}),"\n",(0,r.jsx)(n.li,{children:"Users with strict requirements likely disable relaxation"}),"\n",(0,r.jsx)(n.li,{children:"Simplicity preferred over edge case optimization"}),"\n"]}),"\n",(0,r.jsx)(n.h4,{id:"2-linear-distance-scaling",children:"2. Linear Distance Scaling"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Current:"})," Linear scaling may be too aggressive/conservative"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Alternative:"})," Non-linear curve"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"# Example: Exponential scaling\nscale_factor = 0.25 + 0.75 \xd7 exp(-5 \xd7 (flex - 0.20))\n\n# Or: Sigmoid scaling\nscale_factor = 0.25 + 0.75 / (1 + exp(10 \xd7 (flex - 0.35)))\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Why Not Implemented:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Linear is easier to reason about"}),"\n",(0,r.jsx)(n.li,{children:"No evidence that non-linear is better"}),"\n",(0,r.jsx)(n.li,{children:"Would need extensive testing"}),"\n"]}),"\n",(0,r.jsx)(n.h4,{id:"3-no-temporal-distribution-consideration",children:"3. No Temporal Distribution Consideration"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Issue:"})," May find all periods in one part of day"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Example:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:'All 3 "best price" periods between 02:00-08:00'}),"\n",(0,r.jsx)(n.li,{children:"No periods in evening (when user might want to run appliances)"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Possible Solution:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:'Add "spread" parameter (prefer distributed periods)'}),"\n",(0,r.jsx)(n.li,{children:"Weight periods by time-of-day preferences"}),"\n",(0,r.jsx)(n.li,{children:"Consider user's typical usage patterns"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Why Not Implemented:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Adds complexity"}),"\n",(0,r.jsx)(n.li,{children:"Users can work around with multiple automations"}),"\n",(0,r.jsx)(n.li,{children:"Different users have different needs (no one-size-fits-all)"}),"\n"]}),"\n",(0,r.jsx)(n.h4,{id:"4-period-boundary-handling",children:"4. Period Boundary Handling"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Current Behavior:"})," Periods can cross midnight naturally"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Design Principle:"})," Each interval is evaluated using its ",(0,r.jsx)(n.strong,{children:"own day's"})," reference prices (daily min/max/avg)."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Implementation:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"# In period_building.py build_periods():\nfor price_data in all_prices:\n starts_at = time.get_interval_time(price_data)\n date_key = starts_at.date()\n\n # CRITICAL: Use interval's own day, not period_start_date\n ref_date = date_key\n\n criteria = TibberPricesIntervalCriteria(\n ref_price=ref_prices[ref_date], # Interval's day\n avg_price=avg_prices[ref_date], # Interval's day\n flex=flex,\n min_distance_from_avg=min_distance_from_avg,\n reverse_sort=reverse_sort,\n )\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Why Per-Day Evaluation?"})}),"\n",(0,r.jsx)(n.p,{children:"Periods can cross midnight (e.g., 23:45 \u2192 01:00). Each day has independent reference prices calculated from its 96 intervals."}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Example showing the problem with period-start-day approach:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"Day 1 (2025-11-21): Cheap day\n daily_min = 10 ct, daily_avg = 20 ct, flex = 15%\n Criteria: price \u2264 11.5 ct (10 + 10\xd70.15)\n\nDay 2 (2025-11-22): Expensive day\n daily_min = 20 ct, daily_avg = 30 ct, flex = 15%\n Criteria: price \u2264 23 ct (20 + 20\xd70.15)\n\nPeriod crossing midnight: 23:45 Day 1 \u2192 00:15 Day 2\n 23:45 (Day 1): 11 ct \u2192 \u2705 Passes (11 \u2264 11.5)\n 00:00 (Day 2): 21 ct \u2192 Should this pass?\n\n\u274c WRONG (using period start day):\n 00:00 evaluated against Day 1's 11.5 ct threshold\n 21 ct > 11.5 ct \u2192 Fails\n But 21ct IS cheap on Day 2 (min=20ct)!\n\n\u2705 CORRECT (using interval's own day):\n 00:00 evaluated against Day 2's 23 ct threshold\n 21 ct \u2264 23 ct \u2192 Passes\n Correctly identified as cheap relative to Day 2\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Trade-off: Periods May Break at Midnight"})}),"\n",(0,r.jsx)(n.p,{children:"When days differ significantly, period can split:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"Day 1: Min=10ct, Avg=20ct, 23:45=11ct \u2192 \u2705 Cheap (relative to Day 1)\nDay 2: Min=25ct, Avg=35ct, 00:00=21ct \u2192 \u274c Expensive (relative to Day 2)\nResult: Period stops at 23:45, new period starts later\n"})}),"\n",(0,r.jsxs)(n.p,{children:["This is ",(0,r.jsx)(n.strong,{children:"mathematically correct"})," - 21ct is genuinely expensive on a day where minimum is 25ct."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Market Reality Explains Price Jumps:"})}),"\n",(0,r.jsx)(n.p,{children:"Day-ahead electricity markets (EPEX SPOT) set prices at 12:00 CET for all next-day hours:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Late intervals (23:45): Priced ~36h before delivery \u2192 high forecast uncertainty \u2192 risk premium"}),"\n",(0,r.jsx)(n.li,{children:"Early intervals (00:00): Priced ~12h before delivery \u2192 better forecasts \u2192 lower risk buffer"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"This explains why absolute prices jump at midnight despite minimal demand changes."}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"User-Facing Solution (Nov 2025):"})}),"\n",(0,r.jsx)(n.p,{children:"Added per-period day volatility attributes to detect when classification changes are meaningful:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"day_volatility_%"}),": Percentage spread (span/avg \xd7 100)"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"day_price_min"}),", ",(0,r.jsx)(n.code,{children:"day_price_max"}),", ",(0,r.jsx)(n.code,{children:"day_price_span"}),": Daily price range (ct/\xf8re)"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"Automations can check volatility before acting:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"condition:\n - condition: template\n value_template: >\n {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }}\n"})}),"\n",(0,r.jsx)(n.p,{children:"Low volatility (< 15%) means classification changes are less economically significant."}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Alternative Approaches Rejected:"})}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Use period start day for all intervals"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Problem: Mathematically incorrect - lends cheap day's criteria to expensive day"}),"\n",(0,r.jsx)(n.li,{children:"Rejected: Violates relative evaluation principle"}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Adjust flex/distance at midnight"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Problem: Complex, unpredictable, hides market reality"}),"\n",(0,r.jsx)(n.li,{children:"Rejected: Users should understand price context, not have it hidden"}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Split at midnight always"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Problem: Artificially fragments natural periods"}),"\n",(0,r.jsx)(n.li,{children:"Rejected: Worse user experience"}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Use next day's reference after midnight"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Problem: Period criteria inconsistent across duration"}),"\n",(0,r.jsx)(n.li,{children:"Rejected: Confusing and unpredictable"}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Status:"})," Per-day evaluation is intentional design prioritizing mathematical correctness."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"See Also:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["User documentation: ",(0,r.jsx)(n.code,{children:"docs/user/period-calculation.md"}),' \u2192 "Midnight Price Classification Changes"']}),"\n",(0,r.jsxs)(n.li,{children:["Implementation: ",(0,r.jsx)(n.code,{children:"coordinator/period_handlers/period_building.py"})," (line ~126: ",(0,r.jsx)(n.code,{children:"ref_date = date_key"}),")"]}),"\n",(0,r.jsxs)(n.li,{children:["Attributes: ",(0,r.jsx)(n.code,{children:"coordinator/period_handlers/period_statistics.py"})," (day volatility calculation)"]}),"\n"]}),"\n",(0,r.jsx)(n.hr,{}),"\n",(0,r.jsx)(n.h2,{id:"references",children:"References"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.a,{href:"https://jpawlowski.github.io/hass.tibber_prices/user/period-calculation",children:"User Documentation: Period Calculation"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.a,{href:"/hass.tibber_prices/developer/architecture",children:"Architecture Overview"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.a,{href:"/hass.tibber_prices/developer/caching-strategy",children:"Caching Strategy"})}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.a,{href:"https://github.com/jpawlowski/hass.tibber_prices/blob/main/AGENTS.md",children:"AGENTS.md"})," - AI assistant memory (implementation patterns)"]}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"changelog",children:"Changelog"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"2025-11-19"}),": Initial documentation of Flex/Distance interaction and Relaxation strategy fixes"]}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,l.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(o,{...e})}):o(e)}}}]);