Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v3.3 #708

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ These options can be specified both per-entity and at the top level (affecting a
| non_battery_entity | boolean | `false` | v3.0.0 | Disables default battery state sources e.g. "battery_level" attribute
| default_state_formatting | boolean | `true` | v3.1.0 | Can be used to disable default state formatting e.g. entity display precission setting
| debug | boolean \| string | `false` | v3.2.0 | Whether to show debug output (all available entity data). You can use entity_id if you want to debug specific one.
| respect_visibility_setting | boolean | `true` | v3.3.0 | Whether to hide entities which are marked in the UI as hidden on dashboards.

### Keyword string (KString)

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "battery-state-card",
"version": "3.2.1",
"version": "3.3.0",
"description": "Battery State card for Home Assistant",
"main": "dist/battery-state-card.js",
"author": "Max Chodorowski",
Expand Down
15 changes: 5 additions & 10 deletions src/battery-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ const entititesGlobalProps: (keyof IBatteryEntityConfig)[] = [
"default_state_formatting",
"extend_entity_data",
"icon",
"non_battery_entity",
"round",
"non_battery_entity",
"respect_visibility_setting",
"round",
"secondary_info",
"state_map",
"tap_action",
Expand Down Expand Up @@ -217,11 +218,6 @@ export class BatteryProvider {
*/
private processExcludes() {
if (this.exclude == undefined) {
Object.keys(this.batteries).forEach((entityId) => {
const battery = this.batteries[entityId];
battery.isHidden = (<EntityRegistryDisplayEntry>battery.entityData?.display)?.hidden;
});

return;
}

Expand All @@ -248,8 +244,8 @@ export class BatteryProvider {
}

// we keep the view model to keep updating it
// it might be shown/not-hidden next time
battery.isHidden = isHidden || (<EntityRegistryDisplayEntry>battery.entityData?.display)?.hidden;
// it might be shown/not-hidden after next update
isHidden? battery.hideEntity() : battery.showEntity();
});

toBeRemoved.forEach(entityId => delete this.batteries[entityId]);
Expand All @@ -262,5 +258,4 @@ export interface IBatteryCollection {

export interface IBatteryCollectionItem extends BatteryStateEntity {
entityId?: string;
isHidden?: boolean;
}
56 changes: 43 additions & 13 deletions src/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,26 @@ import { log, safeGetConfigArrayOfObjects } from "./utils";
return config.charging_state.color;
}

if (batteryLevel === undefined || isNaN(batteryLevel) || batteryLevel > 100 || batteryLevel < 0) {
if (batteryLevel === undefined || isNaN(batteryLevel)) {
return defaultColor;
}

const colorSteps = safeGetConfigArrayOfObjects(config.colors?.steps, "color");

if (config.colors?.gradient) {
return getGradientColors(colorSteps, batteryLevel);
return getGradientColors(colorSteps, batteryLevel, config.colors?.non_percent_values);
}

let thresholds: IColorSteps[] = defaultColorSteps;
if (config.colors?.steps) {
// making sure the value is always set
thresholds = colorSteps.map(s => {
s.value = s.value === undefined || s.value > 100 ? 100 : s.value;
s.value = s.value === undefined ? 100 : s.value;
return s;
});
}

return thresholds.find(th => batteryLevel <= th.value!)?.color || defaultColor;
return thresholds.find(th => batteryLevel <= th.value!)?.color || lastObject(thresholds).color || defaultColor;
}

/**
Expand All @@ -41,15 +41,15 @@ import { log, safeGetConfigArrayOfObjects } from "./utils";
* @param level Battery level
* @returns Hex HTML color
*/
const getGradientColors = (config: IColorSteps[], level: number): string => {
const getGradientColors = (config: IColorSteps[], level: number, nonPercentValues?: boolean): string => {

let simpleList = config.map(s => s.color);
if (!isColorGradientValid(simpleList)) {
let colorList = config.map(s => s.color);
if (!isColorGradientValid(colorList)) {
log("For gradient colors you need to use hex HTML colors. E.g. '#FF00FF'", "error");
return defaultColor;
}

if (simpleList.length < 2) {
if (colorList.length < 2) {
log("For gradient colors you need to specify at least two steps/colors", "error");
return defaultColor;
}
Expand All @@ -62,20 +62,28 @@ const getGradientColors = (config: IColorSteps[], level: number): string => {
return first.color;
}

const last = config[config.length - 1];
const last = lastObject(config);
if (level >= last.value!) {
return last.color;
}

const index = config.findIndex(s => level <= s.value!);
if (index != -1) {
simpleList = [ config[index - 1].color, config[index].color ];
colorList = [ config[index - 1].color, config[index].color ];
// calculate percentage
level = (level - config[index - 1].value!) * 100 / (config[index].value! - config[index - 1].value!);
}
// checking whether we should convert the level to the percentage
else if ((nonPercentValues == undefined && config.some(s => s.value! < 0 || s.value! > 100)) || nonPercentValues === true) {
level = convertToPercentage(config, level);
}
}
else if (level < 0 || level > 100) {
log("Entity state value seems to be outside of 0-100 range and color step values are not defined");
return defaultColor;
}

return getColorInterpolationForPercentage(simpleList, level);
return getColorInterpolationForPercentage(colorList, level);
}

/**
Expand Down Expand Up @@ -153,7 +161,7 @@ const getColorInterpolationForPercentage = function (colors: string[], pct: numb
const isColorGradientValid = (gradientColors: string[]) => {
if (gradientColors.length < 2) {
log("Value for 'color_gradient' should be an array with at least 2 colors.");
return;
return false;
}

for (const color of gradientColors) {
Expand All @@ -164,4 +172,26 @@ const getColorInterpolationForPercentage = function (colors: string[], pct: numb
}

return true;
}
}

/**
* Convert given value to percentage (position between min/max step value)
* @param colorSteps Configured steps
* @param value Value to convert
* @returns Percentage
*/
const convertToPercentage = (colorSteps: IColorSteps[], value: number) => {
const values = colorSteps.map((s, i) => s.value === undefined ? i : s.value).sort((a, b) => a - b);

const range = values[values.length - 1] - values[0];
const valueAdjusted = value - values[0];

return Math.round(valueAdjusted / range * 100);
}

/**
* Returns last object in the collection or default
* @param collelction Array of objects
* @returns Last object in the collection or default
*/
const lastObject = <T>(collelction: T[]): T => collelction && collelction.length > 0 ? collelction[collelction.length - 1] : <T>{};
36 changes: 34 additions & 2 deletions src/custom-elements/battery-state-entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { getChargingState } from "../entity-fields/charging-state";
import { getBatteryLevel } from "../entity-fields/battery-level";
import { getName } from "../entity-fields/get-name";
import { getIcon } from "../entity-fields/get-icon";
import { DeviceRegistryEntry } from "../type-extensions";
import { DeviceRegistryEntry, EntityRegistryDisplayEntry } from "../type-extensions";

/**
* Battery entity element
Expand Down Expand Up @@ -61,6 +61,11 @@ export class BatteryStateEntity extends LovelaceCard<IBatteryEntityConfig> {
@property({ attribute: false })
public action: IAction | undefined;

/**
* Whether entity should not be shown
*/
public isHidden: boolean | undefined;

/**
* Raw entity data
*/
Expand All @@ -79,12 +84,25 @@ export class BatteryStateEntity extends LovelaceCard<IBatteryEntityConfig> {
}

async internalUpdate() {

if (!this.hass?.states[this.config.entity]) {
this.alert = {
type: "warning",
title: this.hass?.localize("ui.panel.lovelace.warning.entity_not_found", "entity", this.config.entity) || `Entity not available: ${this.config.entity}`,
}

return;
}

this.entityData = <any>{
...this.hass?.states[this.config.entity]
...this.hass.states[this.config.entity]
};

if (this.config.extend_entity_data !== false) {
this.extendEntityData();

// make sure entity is visible when it should be shown
this.showEntity();
}

if (this.config.debug === true || this.config.debug === this.config.entity) {
Expand Down Expand Up @@ -127,6 +145,20 @@ export class BatteryStateEntity extends LovelaceCard<IBatteryEntityConfig> {
onError(): void {
}

hideEntity(): void {
this.isHidden = true;
}

showEntity(): void {
if (this.config.respect_visibility_setting !== false && (<EntityRegistryDisplayEntry>this.entityData?.display)?.hidden) {
// When entity is marked as hidden in the UI we should respect it
this.isHidden = true;
return;
}

this.isHidden = false;
}

/**
* Adding or removing action
* @param enable Whether to enable/add the tap action
Expand Down
6 changes: 3 additions & 3 deletions src/entity-fields/battery-level.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const formattedStatePattern = /(-?[0-9,.]+)\s?(.*)/;
* @param hass HomeAssistant state object
* @returns Battery level
*/
export const getBatteryLevel = (config: IBatteryEntityConfig, hass: HomeAssistantExt | undefined, entityData: IMap<any> | undefined): IBatteryState => {
export const getBatteryLevel = (config: IBatteryEntityConfig, hass: HomeAssistantExt, entityData: IMap<any> | undefined): IBatteryState => {
const UnknownLevel = hass?.localize("state.default.unknown") || "Unknown";
let state: string;
let unit: string | undefined;
Expand Down Expand Up @@ -50,8 +50,8 @@ export const getBatteryLevel = (config: IBatteryEntityConfig, hass: HomeAssistan
}
else {
const candidates: (string | number | undefined)[] = [
config.non_battery_entity ? null: entityData.attributes?.battery_level,
config.non_battery_entity ? null: entityData.attributes?.battery,
config.non_battery_entity ? null: entityData.attributes.battery_level,
config.non_battery_entity ? null: entityData.attributes.battery,
entityData.state
];

Expand Down
7 changes: 1 addition & 6 deletions src/entity-fields/charging-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,7 @@ import { log, safeGetArray } from "../utils";
* @param hass HomeAssistant state object
* @returns Whether battery is in chargin mode
*/
export const getChargingState = (config: IBatteryEntityConfig, state: string, hass?: HomeAssistant): boolean => {

if (!hass) {
return false;
}

export const getChargingState = (config: IBatteryEntityConfig, state: string, hass: HomeAssistant): boolean => {
const chargingConfig = config.charging_state;
if (!chargingConfig) {
return getDefaultChargingState(config, hass);
Expand Down
6 changes: 3 additions & 3 deletions src/entity-fields/get-icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import { RichStringProcessor } from "../rich-string-processor";
* @param hass HomeAssistant state object
* @returns Mdi icon string
*/
export const getIcon = (config: IBatteryEntityConfig, level: number | undefined, isCharging: boolean, hass: HomeAssistant | undefined): string => {
export const getIcon = (config: IBatteryEntityConfig, level: number | undefined, isCharging: boolean, hass: HomeAssistant): string => {
if (isCharging && config.charging_state?.icon) {
return config.charging_state.icon;
}

if (config.icon) {
const attribPrefix = "attribute.";
// check if we should return the icon/string from the attribute value
if (hass && config.icon.startsWith(attribPrefix)) {
if (config.icon.startsWith(attribPrefix)) {
const attribName = config.icon.substr(attribPrefix.length);
const val = hass.states[config.entity].attributes[attribName] as string | undefined;
if (!val) {
Expand All @@ -29,7 +29,7 @@ export const getIcon = (config: IBatteryEntityConfig, level: number | undefined,
return val;
}

const processor = new RichStringProcessor(hass, { ...hass?.states[config.entity] });
const processor = new RichStringProcessor(hass, { ...hass.states[config.entity] });
return processor.process(config.icon);
}

Expand Down
4 changes: 2 additions & 2 deletions src/entity-fields/get-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import { RichStringProcessor } from "../rich-string-processor";
* @param hass HomeAssistant state object
* @returns Battery name
*/
export const getName = (config: IBatteryEntityConfig, hass: HomeAssistant | undefined, entityData: IMap<any> | undefined): string => {
export const getName = (config: IBatteryEntityConfig, hass: HomeAssistant, entityData: IMap<any>): string => {
if (config.name) {
const proc = new RichStringProcessor(hass, entityData);
return proc.process(config.name);
}

let name = entityData?.attributes?.friendly_name;
let name = entityData.attributes.friendly_name;

// when we have failed to get the name we just return entity id
if (!name) {
Expand Down
2 changes: 1 addition & 1 deletion src/entity-fields/get-secondary-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { isNumber } from "../utils";
* @param entidyData Entity data
* @returns Secondary info text
*/
export const getSecondaryInfo = (config: IBatteryEntityConfig, hass: HomeAssistant | undefined, entityData: IMap<any> | undefined): string => {
export const getSecondaryInfo = (config: IBatteryEntityConfig, hass: HomeAssistant, entityData: IMap<any> | undefined): string => {
if (config.secondary_info) {
const processor = new RichStringProcessor(hass, entityData);

Expand Down
4 changes: 2 additions & 2 deletions src/rich-string-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const validEntityDomains = [
*/
export class RichStringProcessor {

constructor(private hass: HomeAssistant | undefined, private entityData: IMap<any> | undefined) {
constructor(private hass: HomeAssistant, private entityData: IMap<any> | undefined) {
}

/**
Expand Down Expand Up @@ -87,7 +87,7 @@ const validEntityDomains = [

if (validEntityDomains.includes(chunks[0])) {
data = {
...this.hass?.states[chunks.splice(0, 2).join(".")]
...this.hass.states[chunks.splice(0, 2).join(".")]
};
}

Expand Down
9 changes: 9 additions & 0 deletions src/typings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ interface IColorSettings {
* Whether to enable smooth color transition between steps
*/
gradient?: boolean;
/**
* Whether the values are not percentages
*/
non_percent_values?: boolean;
}

/**
Expand Down Expand Up @@ -269,6 +273,11 @@ interface IBatteryEntityConfig {
* Whether to print the debug output
*/
debug?: string | boolean,

/**
* Whether to respect HA entity visibility setting
*/
respect_visibility_setting?: boolean,
}

interface IBatteryCardConfig {
Expand Down
Loading
Loading