Skip to content

Commit

Permalink
you can specify the battery charge cut-off capacity using the soc_end…
Browse files Browse the repository at this point in the history
…_of_charge attribute. If for example you set your battery to charge up to 90% the card will correctly display charge time to this capacity. Expects a numeric value between 80 and 100 or sensor i.e. sensor.soc_end_of_charge by slipx06 (sunsynk-power-flow-card#514)
  • Loading branch information
molikk committed Oct 22, 2024
1 parent e4fdcfe commit 377b4b5
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 146 deletions.
2 changes: 1 addition & 1 deletion src/cards/compact-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export const compactCard = (config: PowerFlowCardConfig, inverterImg: string, da
${Inverter.generateTimerInfo(data, config)}
${Inverter.generatePriorityLoad(data, config)}
${Inverter.generateInverterImage(data, inverterImg)}
${Inverter.generateInverterImage(data, config, inverterImg)}
${Inverter.generateInverterState(data, config)}
${Inverter.generateInverterLoad(data, config)}
${Inverter.generateInverterProgram(data)}
Expand Down
11 changes: 8 additions & 3 deletions src/cards/compact/battery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,11 @@ export class Battery {
<svg id="battery-flow">
<path id="bat-line"
d="M 239 250 L 239 ${y}" fill="none"
stroke="${config.battery.dynamic_colour ? data.flowBatColour : data.batteryColour}" stroke-width="${data.batLineWidth}" stroke-miterlimit="10"
stroke="${Battery.batteryColour(data, config)}" stroke-width="${data.batLineWidth}" stroke-miterlimit="10"
pointer-events="stroke"/>
<circle id="power-dot-discharge" cx="0" cy="0"
r="${Math.min(2 + data.batLineWidth + Math.max(data.minLineWidth - 2, 0), 8)}"
fill="${data.batteryPower <= 0 ? 'transparent' : `${Battery.batteryColour(data, config)}}`}">
fill="${data.batteryPower <= 0 ? 'transparent' : `${Battery.batteryColour(data, config)}`}">
<animateMotion dur="${data.durationCur['battery']}s" repeatCount="indefinite"
keyPoints="1;0" keyTimes="0;1" calcMode="linear">
<mpath xlink:href="#bat-line"/>
Expand Down Expand Up @@ -265,7 +265,7 @@ export class Battery {

static generateBatteryGradient(data: DataDto, config: PowerFlowCardConfig) {
const y = Battery.showOuterBatteryBanks(config)?312.5 : 325.5;
return svg`
let bat = svg`
<svg xmlns="http://www.w3.org/2000/svg" id="bat" x="212.5"
y="${y}" width="78.75"
height="78.75" preserveAspectRatio="none"
Expand Down Expand Up @@ -307,5 +307,10 @@ export class Battery {
d="${data.batteryCharge}"/>
</svg>
`;
return config.battery.navigate?
svg` <a href="#" @click=${(e) => Utils.handleNavigation(e, config.battery.navigate)}>
${bat}
</a>`
: bat;
}
}
306 changes: 164 additions & 142 deletions src/helpers/utils.ts
Original file line number Diff line number Diff line change
@@ -1,146 +1,168 @@
import {unitOfEnergyConversionRules, UnitOfEnergyOrPower, UnitOfPower} from '../const';
import { unitOfEnergyConversionRules, UnitOfEnergyOrPower, UnitOfPower } from '../const';
import { navigate } from 'custom-card-helpers';

export class Utils {
static toNum(val: string | number, decimals: number = -1, invert: boolean = false, abs: boolean = false): number {
let numberValue = Number(val);
if (Number.isNaN(numberValue)) {
return 0;
}
if (decimals >= 0) {
numberValue = parseFloat(numberValue.toFixed(decimals));
}
if (invert) {
numberValue *= -1;
}
if(abs) {
numberValue = Math.abs(numberValue);
}
return numberValue;
}

static convertValue(value, decimal = 2) {
decimal = Number.isNaN(decimal) ? 2 : decimal;
if (Math.abs(value) >= 1000000) {
return `${(value / 1000000).toFixed(decimal)} MW`;
} else if (Math.abs(value) >= 1000) {
return `${(value / 1000).toFixed(decimal)} kW`;
} else {
return `${Math.round(value)} W`;
}
}

static convertValueNew(
value: string | number,
unit: UnitOfEnergyOrPower | string = '',
decimal: number = 2,
withUnit: boolean = true,
):string {
decimal = isNaN(decimal) ? 2 : decimal;
const numberValue = Number(value);
if (isNaN(numberValue)) return Number(0).toFixed(decimal);

const rules = unitOfEnergyConversionRules[unit];
if (!rules) {
if(withUnit) {
return `${this.toNum(numberValue, decimal)} ${unit}`
}
return `${this.toNum(numberValue, decimal)}`;
}

if (unit === UnitOfPower.WATT && Math.abs(numberValue) < 1000) {
if(withUnit) {
return `${Math.round(numberValue)} ${unit}`;
}
return `${Math.round(numberValue)}`;
}

if (unit === UnitOfPower.KILO_WATT && Math.abs(numberValue) < 1) {
if(withUnit) {
return `${Math.round(numberValue * 1000)} W`;
}
return `${Math.round(numberValue * 1000)}`;
}

if (unit === UnitOfPower.MEGA_WATT && Math.abs(numberValue) < 1) {
if(withUnit) {
return `${(numberValue * 1000).toFixed(decimal)} kW`;
}
return `${(numberValue * 1000).toFixed(decimal)}`;
}

for (const rule of rules) {
if (Math.abs(numberValue) >= rule.threshold) {
const convertedValue = (numberValue / rule.divisor).toFixed(rule.decimal || decimal);
if(withUnit) {
return `${convertedValue} ${rule.targetUnit}`;
}
return `${convertedValue}`;
}
}

if(withUnit) {
return `${numberValue.toFixed(decimal)} ${unit}`;
static toNum(val: string | number, decimals: number = -1, invert: boolean = false, abs: boolean = false): number {
let numberValue = Number(val);
if (Number.isNaN(numberValue)) {
return 0;
}
if (decimals >= 0) {
numberValue = parseFloat(numberValue.toFixed(decimals));
}
if (invert) {
numberValue *= -1;
}
if (abs) {
numberValue = Math.abs(numberValue);
}
return numberValue;
}

static convertValue(value, decimal = 2) {
decimal = Number.isNaN(decimal) ? 2 : decimal;
if (Math.abs(value) >= 1000000) {
return `${(value / 1000000).toFixed(decimal)} MW`;
} else if (Math.abs(value) >= 1000) {
return `${(value / 1000).toFixed(decimal)} kW`;
} else {
return `${Math.round(value)} W`;
}
}

static convertValueNew(
value: string | number,
unit: UnitOfEnergyOrPower | string = '',
decimal: number = 2,
withUnit: boolean = true,
): string {
decimal = isNaN(decimal) ? 2 : decimal;
const numberValue = Number(value);
if (isNaN(numberValue)) return Number(0).toFixed(decimal);

const rules = unitOfEnergyConversionRules[unit];
if (!rules) {
if (withUnit) {
return `${this.toNum(numberValue, decimal)} ${unit}`;
}
return `${this.toNum(numberValue, decimal)}`;
}

if (unit === UnitOfPower.WATT && Math.abs(numberValue) < 1000) {
if (withUnit) {
return `${Math.round(numberValue)} ${unit}`;
}
return `${Math.round(numberValue)}`;
}

if (unit === UnitOfPower.KILO_WATT && Math.abs(numberValue) < 1) {
if (withUnit) {
return `${Math.round(numberValue * 1000)} W`;
}
return `${Math.round(numberValue * 1000)}`;
}

if (unit === UnitOfPower.MEGA_WATT && Math.abs(numberValue) < 1) {
if (withUnit) {
return `${(numberValue * 1000).toFixed(decimal)} kW`;
}
return `${(numberValue * 1000).toFixed(decimal)}`;
}

for (const rule of rules) {
if (Math.abs(numberValue) >= rule.threshold) {
const convertedValue = (numberValue / rule.divisor).toFixed(rule.decimal || decimal);
if (withUnit) {
return `${convertedValue} ${rule.targetUnit}`;
}
return `${numberValue.toFixed(decimal)}`;
}

private static isPopupOpen = false;
static handlePopup(event, entityId) {
if (!entityId) {
return;
}
event.preventDefault();
this._handleClick(event, { action: 'more-info' }, entityId);
}

private static _handleClick(event, actionConfig, entityId) {
if (!event || !entityId) {
return;
}

event.stopPropagation();

// Handle different actions based on actionConfig
switch (actionConfig.action) {
case 'more-info':
this._dispatchMoreInfoEvent(event, entityId);
break;
default:
console.warn(`Action '${actionConfig.action}' is not supported.`);
}
}

private static _dispatchMoreInfoEvent(event, entityId) {

if (Utils.isPopupOpen) {
return;
}

Utils.isPopupOpen = true;

const moreInfoEvent = new CustomEvent('hass-more-info', {
composed: true,
detail: { entityId },
});

history.pushState({ popupOpen: true }, '', window.location.href);

event.target.dispatchEvent(moreInfoEvent);

const closePopup = () => {

if (Utils.isPopupOpen) {
//console.log(`Closing popup for entityId: ${entityId}`);
Utils.isPopupOpen = false;

// Remove the event listener to avoid multiple bindings
window.removeEventListener('popstate', closePopup);

// Optionally, if your popup close logic doesn't trigger history.back(), call it manually
history.back();
}
};

window.addEventListener('popstate', closePopup, { once: true });
}
return `${convertedValue}`;
}
}

if (withUnit) {
return `${numberValue.toFixed(decimal)} ${unit}`;
}
return `${numberValue.toFixed(decimal)}`;
}

private static isPopupOpen = false;

static handlePopup(event, entityId) {
if (!entityId) {
return;
}
event.preventDefault();
this._handleClick(event, { action: 'more-info' }, entityId);
}

static handleNavigation(event, navigationPath) {
if (!navigationPath) {
return;
}
event.preventDefault();
this._handleClick(event, { action: 'navigate', navigation_path: navigationPath }, null);
}

private static _handleClick(event, actionConfig, entityId) {
if (!event || (!entityId && !actionConfig.navigation_path)) {
return;
}

event.stopPropagation();

// Handle different actions based on actionConfig
switch (actionConfig.action) {
case 'more-info':
this._dispatchMoreInfoEvent(event, entityId);
break;
case 'navigate':
this._handleNavigationEvent(event, actionConfig.navigation_path);
break;
default:
console.warn(`Action '${actionConfig.action}' is not supported.`);
}
}

private static _dispatchMoreInfoEvent(event, entityId) {

if (Utils.isPopupOpen) {
return;
}

Utils.isPopupOpen = true;

const moreInfoEvent = new CustomEvent('hass-more-info', {
composed: true,
detail: { entityId },
});

history.pushState({ popupOpen: true }, '', window.location.href);

event.target.dispatchEvent(moreInfoEvent);

const closePopup = () => {

if (Utils.isPopupOpen) {
//console.log(`Closing popup for entityId: ${entityId}`);
Utils.isPopupOpen = false;

// Remove the event listener to avoid multiple bindings
window.removeEventListener('popstate', closePopup);

// Optionally, if your popup close logic doesn't trigger history.back(), call it manually
history.back();
}
};

window.addEventListener('popstate', closePopup, { once: true });
}

private static _handleNavigationEvent(event, navigationPath) {
// Perform the navigation action
if (navigationPath) {
navigate(event.target, navigationPath); // Assuming 'navigate' is a function available in your environment
} else {
console.warn('Navigation path is not provided.');
}
}
}
2 changes: 2 additions & 0 deletions src/localize/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@
"invert_load": "Invert Values",
"shutdown_soc": "Shutdown SOC",
"shutdown_soc_offgrid": "Shutdown SOC (Off Grid)",
"soc_end_of_charge": "SOC End of Charge",
"navigate": "Navigation Path",
"energy": "Energy",
"auto_scale": "Auto Scale",
"three_phase": "Three Phase",
Expand Down
Loading

0 comments on commit 377b4b5

Please sign in to comment.