Compare commits

...

11 Commits

Author SHA1 Message Date
Petar Petrov 8f86d4c855 Solar scene: single battery chip, reworked chip cluster
- Merge the battery power and SoC chips into one: the SoC drives the
  battery level icon (like the energy distribution card), the value is
  the battery power, and the chip colour shows charge vs discharge.
- Battery flows: no battery -> home line while charging; bead from PV
  or grid (whichever feeds the charge) into the battery; static home
  link when idle.
- Chip cluster: the home chip sits over the house, grid and battery
  flank it halfway up to the PV chip.
- Drop the low-carbon chip and its now-unused plumbing.
- Add the card without a default title in the Now view strategy.
2026-07-06 11:51:11 +03:00
Petar Petrov e5c8c4939a Add tests for solar scene card gating helpers 2026-07-03 16:45:27 +03:00
Petar Petrov 8f4eb4e331 Merge branch 'dev' into feat/solar-scene-snapshot-card 2026-07-03 16:41:04 +03:00
Petar Petrov d29ef73162 Fix Prettier formatting in solar scene card 2026-07-03 16:23:28 +03:00
Jérôme CREMOUX 85f22d5103 Gate the solar scene card on a configured location in the strategy
Move the hasLocation helper into energy-cards.ts (generic, hass-based) and
gate the solar scene card on it in power-view-strategy, so it is not even
added when no home location is configured. The card keeps its own render
guard as a fallback. Also wrap the px-per-metre line so it passes prettier.
2026-07-01 13:07:20 +02:00
Jérôme CREMOUX 2f0fc6b01e Merge branch 'dev' into feat/solar-scene-snapshot-card 2026-07-01 12:07:35 +02:00
Jérôme CREMOUX 32c90fe410 Solar scene card: show only configured live power
- Live power comes only from the configured stat_rate sensors (solar, grid,
  battery); removed the fallback that derived power from the latest energy
  bucket.
- Gate the card on a solar source with a stat_rate (new hasSolarPower helper).
- Render nothing when no home location is configured (new hasLocation check);
  removed the default-position fallback.
- willUpdate redraws on live-power change so a chip and its connection toggle
  together instead of waiting for the minute tick.
- Use the TILE_PX constant in the px-per-metre formula.
2026-07-01 12:05:36 +02:00
Jérôme CREMOUX a7a182059f Face the house front by default + lighter outline 2026-07-01 05:29:35 +02:00
Jérôme CREMOUX d2d03381d8 Add CARTO/OpenStreetMap basemap attribution
CARTO's terms and OSM's ODbL require visible credit whenever their tiles are
shown. Add a small attribution in the bottom-right corner, only when the live
basemap is rendered (not on the offline flat-ground fallback).
2026-07-01 05:16:29 +02:00
Jérôme CREMOUX b3972eea33 Update energy-cards.ts
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-06-30 08:10:33 +02:00
Jérôme CREMOUX 89c3c42d66 Add Solar scene card to the Energy Now tab 2026-06-29 14:14:31 +02:00
6 changed files with 2010 additions and 0 deletions
@@ -4,6 +4,7 @@ import type {
GridSourceTypeEnergyPreference,
} from "../../../data/energy";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import type { HomeAssistant } from "../../../types";
/** Strategy config shared by the per-view energy strategies. */
export interface EnergyViewStrategyConfig extends LovelaceStrategyConfig {
@@ -36,6 +37,22 @@ export const hasReturn = (prefs: EnergyPreferences): boolean =>
export const hasSolar = (prefs: EnergyPreferences): boolean =>
prefs.energy_sources.some((source) => source.type === "solar");
/** Solar source with a live power sensor (`stat_rate`); the "now" scene card shows live power only. */
export const hasSolarPower = (prefs: EnergyPreferences): boolean =>
prefs.energy_sources.some(
(source) => source.type === "solar" && !!source.stat_rate
);
/** A home location is configured when both coordinates are finite and not the meaningless (0, 0) origin. */
export const hasLocation = (hass: HomeAssistant): boolean => {
const { latitude, longitude } = hass.config;
return (
Number.isFinite(latitude) &&
Number.isFinite(longitude) &&
!(latitude === 0 && longitude === 0)
);
};
export const hasBattery = (prefs: EnergyPreferences): boolean =>
prefs.energy_sources.some((source) => source.type === "battery");
@@ -262,6 +279,12 @@ export const ENERGY_CARD_CATALOG: readonly EnergyCardCatalogEntry[] = [
),
// --- Now (power) ---
entry(
"now",
"energy-solar-scene-now",
"ui.panel.energy.cards.energy_solar_scene_title",
(p) => hasSolarPower(p)
),
entry(
"now",
"power-sources-graph",
@@ -9,6 +9,7 @@ import type { HomeAssistant } from "../../../types";
import type { EnergyViewStrategyConfig } from "./energy-cards";
import {
hasGasRateSource,
hasLocation,
hasPowerDevices,
hasPowerSources,
hasWaterRateDevices,
@@ -71,6 +72,21 @@ export class PowerViewStrategy extends ReactiveElement {
return view;
}
// Solar scene (snapshot): a live, present-instant 3D view, pinned to the top of the Now tab. Needs a home
// location to place the house, so it is skipped entirely when none is configured.
if (
hasLocation(hass) &&
isEnergyCardVisible("now", "energy-solar-scene-now", prefs, hidden)
) {
chartsSection.cards!.push({
type: "energy-solar-scene-now",
collection_key: collectionKey,
grid_options: {
columns: 36,
},
});
}
if (hasPowerSrc) {
badges.push({
type: "power-total",
File diff suppressed because it is too large Load Diff
@@ -50,6 +50,8 @@ const LAZY_LOAD_TYPES = {
import("../cards/energy/hui-energy-devices-detail-graph-card"),
"energy-distribution": () =>
import("../cards/energy/hui-energy-distribution-card"),
"energy-solar-scene-now": () =>
import("../cards/energy/hui-energy-solar-scene-now-card"),
"energy-gas-graph": () => import("../cards/energy/hui-energy-gas-graph-card"),
"energy-water-graph": () =>
import("../cards/energy/hui-energy-water-graph-card"),
+10
View File
@@ -11297,6 +11297,16 @@
"energy_gas_graph_title": "Gas consumption",
"energy_water_graph_title": "Water consumption",
"energy_distribution_title": "Energy distribution",
"energy_solar_scene_title": "Solar scene",
"energy_solar_scene_a11y": {
"solar": "solar production {value}",
"home": "home consumption {value}",
"grid_import": "grid import {value}",
"grid_export": "grid export {value}",
"battery_charge": "battery charging {value}",
"battery_discharge": "battery discharging {value}",
"battery_soc": "battery charge {value}"
},
"energy_sources_table_title": "Sources",
"energy_devices_graph_title": "Individual devices total usage",
"energy_devices_detail_graph_title": "Individual devices detail usage",
@@ -3,12 +3,15 @@ import type {
EnergyPreferences,
EnergySource,
} from "../../../../src/data/energy";
import type { HomeAssistant } from "../../../../src/types";
import {
applicableEnergyCardKeys,
ENERGY_CARD_CATALOG,
energyCardKey,
hasEnergySource,
hasGasRateSource,
hasLocation,
hasSolarPower,
hasWaterRateSource,
isEnergyCardHidden,
isEnergyCardVisible,
@@ -225,3 +228,42 @@ describe("isEnergyCardVisible", () => {
}
});
});
describe("hasSolarPower", () => {
const SOLAR_RATE = source({
type: "solar",
stat_energy_from: "sensor.solar",
stat_rate: "sensor.solar_power",
});
it("requires a solar source with a live rate sensor", () => {
expect(hasSolarPower(makePrefs({ energy_sources: [SOLAR_RATE] }))).toBe(
true
);
// Solar configured for energy only (no stat_rate) -> nothing live for the "now" card.
expect(hasSolarPower(makePrefs({ energy_sources: [SOLAR] }))).toBe(false);
// No solar source at all.
expect(hasSolarPower(makePrefs({ energy_sources: [GRID_RETURN] }))).toBe(
false
);
});
});
describe("hasLocation", () => {
const hassAt = (latitude: number, longitude: number): HomeAssistant =>
({ config: { latitude, longitude } }) as unknown as HomeAssistant;
it("is true for real coordinates, including negative ones", () => {
expect(hasLocation(hassAt(52.37, 4.9))).toBe(true);
expect(hasLocation(hassAt(-33.87, -70.66))).toBe(true);
});
it("is false at the meaningless (0, 0) origin", () => {
expect(hasLocation(hassAt(0, 0))).toBe(false);
});
it("is false when a coordinate is not finite", () => {
expect(hasLocation(hassAt(NaN, 4.9))).toBe(false);
expect(hasLocation(hassAt(52.37, Infinity))).toBe(false);
});
});