|
1 | 1 | import { describe, it, expect, afterEach } from 'vitest' |
2 | | -import { computeLive, cachedCollector } from '../src/solar.js' |
| 2 | +import { computeLive, cachedCollector, buildDaySeries } from '../src/solar.js' |
3 | 3 | import { _setForTests } from '../src/install.js' |
4 | 4 | import { LEGACY_TOPOLOGY, GENERIC_TOPOLOGY } from '../src/install.js' |
5 | 5 |
|
@@ -100,6 +100,92 @@ describe('computeLive — topología genérica (issue #37)', () => { |
100 | 100 | }) |
101 | 101 | }) |
102 | 102 |
|
| 103 | +describe('issue #135 — grid derivado con entradas caídas', () => { |
| 104 | + // HA conectado con la topología LEGACY (3 medidores de consumo en W, |
| 105 | + // batería con powerId/socId, grid por attrs del shim). |
| 106 | + function haLegacy(overrides = {}) { |
| 107 | + const map = { |
| 108 | + 'sensor.solis_potencia_actual': { state: '0' }, |
| 109 | + 'sensor.almacen_pinza_power_b': { state: '0' }, |
| 110 | + 'sensor.medidor_respaldo_power': { state: '500' }, |
| 111 | + 'sensor.vivienda_medidor_power': { state: '200' }, |
| 112 | + 'sensor.almacen_pinza_power_a': { state: '50' }, |
| 113 | + 'sensor.solis_bateria_potencia': { state: '0' }, |
| 114 | + 'sensor.solis_bateria_soc': { state: '50' }, |
| 115 | + 'sensor.solis_bateria_estado': { state: 'Descargando' }, |
| 116 | + 'sensor.solis_scraper': { state: 'ok', attributes: { currentGridPower: 0.7, gridDirection: 'import' } }, |
| 117 | + 'sun.sun': { state: 'below_horizon', attributes: { elevation: -20 } }, |
| 118 | + ...overrides, |
| 119 | + } |
| 120 | + return { connected: true, getState: (id) => map[id] } |
| 121 | + } |
| 122 | + |
| 123 | + it('computeLive: un medidor de consumo caído → consumption null + alerta, grid intacto', () => { |
| 124 | + _setForTests(LEGACY_TOPOLOGY) |
| 125 | + const live = computeLive(haLegacy({ 'sensor.medidor_respaldo_power': { state: 'unavailable' } })) |
| 126 | + expect(live.consumption).toBeNull() |
| 127 | + expect(live.alerts.some((a) => a.id === 'consumo' && a.severity === 'warning')).toBe(true) |
| 128 | + expect(live.grid).toBeCloseTo(0.7, 2) // la fuente de red sigue viva |
| 129 | + }) |
| 130 | + |
| 131 | + it('computeLive: fuente de red (attrs) caída → grid null, sin fallback inventado', () => { |
| 132 | + _setForTests(LEGACY_TOPOLOGY) |
| 133 | + const live = computeLive(haLegacy({ 'sensor.solis_scraper': { state: 'unavailable', attributes: {} } })) |
| 134 | + expect(live.grid).toBeNull() |
| 135 | + }) |
| 136 | + |
| 137 | + // Buckets de noche que reproducen el caso real del 26-Sep: medidores de |
| 138 | + // consumo caídos y batería descargando 3,3 kW. Antes: grid -3,3 kW |
| 139 | + // (exportación imposible de noche). Ahora: null. |
| 140 | + it('buildDaySeries: bucket sin medidores de consumo → grid null (no export imposible)', () => { |
| 141 | + _setForTests(LEGACY_TOPOLOGY) |
| 142 | + const stats = { |
| 143 | + 'sensor.medidor_respaldo_power': [ |
| 144 | + { start: '2026-09-26T19:55:00', mean: 500 }, |
| 145 | + { start: '2026-09-26T20:05:00', mean: 500 }, |
| 146 | + ], |
| 147 | + 'sensor.vivienda_medidor_power': [ |
| 148 | + { start: '2026-09-26T19:55:00', mean: 200 }, |
| 149 | + { start: '2026-09-26T20:05:00', mean: 200 }, |
| 150 | + ], |
| 151 | + 'sensor.almacen_pinza_power_a': [ |
| 152 | + { start: '2026-09-26T19:55:00', mean: 50 }, |
| 153 | + { start: '2026-09-26T20:05:00', mean: 50 }, |
| 154 | + ], |
| 155 | + 'sensor.solis_bateria_potencia': [ |
| 156 | + { start: '2026-09-26T19:55:00', mean: 3.3 }, |
| 157 | + { start: '2026-09-26T20:00:00', mean: 3.3 }, |
| 158 | + { start: '2026-09-26T20:05:00', mean: 3.3 }, |
| 159 | + ], |
| 160 | + 'sensor.solis_bateria_soc': [ |
| 161 | + { start: '2026-09-26T19:55:00', mean: 50 }, |
| 162 | + { start: '2026-09-26T20:00:00', mean: 49 }, |
| 163 | + { start: '2026-09-26T20:05:00', mean: 49 }, |
| 164 | + ], |
| 165 | + } |
| 166 | + const { points } = buildDaySeries(stats, '2026-09-26', null, {}) |
| 167 | + const p1955 = points.find((p) => p.label === '19:55') |
| 168 | + const p2000 = points.find((p) => p.label === '20:00') |
| 169 | + const p2005 = points.find((p) => p.label === '20:05') |
| 170 | + expect(p1955.grid).toBeCloseTo(0.75, 2) // todo presente: 0,75 kW de import |
| 171 | + expect(p2000.grid).toBeNull() // medidores caídos + batería descargando: antes -3,3 kW |
| 172 | + expect(p2005.grid).toBeCloseTo(0.75, 2) // medidores recuperados |
| 173 | + }) |
| 174 | + |
| 175 | + it('buildDaySeries: falta UN medidor de consumo (hueco parcial) → grid null', () => { |
| 176 | + _setForTests(LEGACY_TOPOLOGY) |
| 177 | + const stats = { |
| 178 | + 'sensor.medidor_respaldo_power': [{ start: '2026-09-26T21:00:00', mean: 500 }], |
| 179 | + 'sensor.almacen_pinza_power_a': [{ start: '2026-09-26T21:00:00', mean: 50 }], |
| 180 | + 'sensor.solis_bateria_potencia': [{ start: '2026-09-26T21:00:00', mean: 0 }], |
| 181 | + 'sensor.solis_bateria_soc': [{ start: '2026-09-26T21:00:00', mean: 50 }], |
| 182 | + } |
| 183 | + const { points } = buildDaySeries(stats, '2026-09-26', null, {}) |
| 184 | + expect(points).toHaveLength(1) |
| 185 | + expect(points[0].grid).toBeNull() // consumo infrarreportado (0,55 en vez de 0,75): no fiable |
| 186 | + }) |
| 187 | +}) |
| 188 | + |
103 | 189 | describe('cachedCollector', () => { |
104 | 190 | it('single-flight: N llamadas concurrentes = 1 ejecución', async () => { |
105 | 191 | let calls = 0 |
|
0 commit comments