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

Use default home assistant state formatting #619

Merged
merged 3 commits into from
Dec 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ These options can be specified both per-entity and at the top level (affecting a
| unit | string | `"%"` | v2.1.0 | Override for unit displayed next to the state/level value ([example](#other-use-cases))
| value_override | [KString](#keyword-string-kstring) | | v3.0.0 | Allows to override the battery level value. Note: when used the `multiplier`, `round`, `state_map` setting is ignored
| non_battery_entity | boolean | `false` | v3.0.0 | Disables default battery state sources e.g. "battery_level" attribute
| default_state_formatting | bollean | `true` | v3.1.0 | Can be used to disable default state formatting e.g. entity display precission setting

### Keyword string (KString)

Expand Down
393 changes: 196 additions & 197 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 8 additions & 4 deletions src/battery-provider.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { getRegexFromString, log, safeGetConfigArrayOfObjects } from "./utils";
import { log, safeGetConfigArrayOfObjects } from "./utils";
import { HomeAssistant } from "custom-card-helpers";
import { BatteryStateEntity } from "./custom-elements/battery-state-entity";
import { Filter } from "./filter";
import { HomeAssistantExt } from "./type-extensions";

/**
* Properties which should be copied over to individual entities from the card
*/
const entititesGlobalProps: (keyof IBatteryEntityConfig)[] = [ "tap_action", "state_map", "charging_state", "secondary_info", "colors", "bulk_rename", "icon", "round", "unit", "value_override", "non_battery_entity" ];
const entititesGlobalProps: (keyof IBatteryEntityConfig)[] = [ "tap_action", "state_map", "charging_state", "secondary_info", "colors", "bulk_rename", "icon", "round", "unit", "value_override", "non_battery_entity", "default_state_formatting" ];

/**
* Class responsible for intializing Battery view models based on given configuration.
Expand All @@ -23,6 +24,9 @@ export class BatteryProvider {
*/
private exclude: Filter[] | undefined;

/**
* Collection of battery HTML elements.
*/
private batteries: IBatteryCollection = {};

/**
Expand Down Expand Up @@ -51,7 +55,7 @@ export class BatteryProvider {
this.processExplicitEntities();
}

async update(hass: HomeAssistant): Promise<void> {
async update(hass: HomeAssistantExt): Promise<void> {
if (!this.initialized) {
// groups and includes should be processed just once
this.initialized = true;
Expand Down Expand Up @@ -183,7 +187,7 @@ export class BatteryProvider {
if (this.batteries[entity_id]) {
return;
}

this.batteries[entity_id] = this.createBattery({ entity: entity_id });
});

Expand Down
6 changes: 3 additions & 3 deletions src/custom-elements/battery-state-entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,14 @@ export class BatteryStateEntity extends LovelaceCard<IBatteryEntityConfig> {
};

this.name = getName(this.config, this.hass);
var { state, level} = getBatteryLevel(this.config, this.hass);
var { state, level, unit_override} = getBatteryLevel(this.config, this.hass);
this.state = state;

if (level !== undefined && this.config.unit !== "" && this.config.unit !== null) {
if (unit_override === undefined && level !== undefined && this.config.unit !== "" && this.config.unit !== null) {
this.unit = String.fromCharCode(160) + (this.config.unit || this.hass?.states[this.config.entity]?.attributes["unit_of_measurement"] || "%");
}
else {
this.unit = undefined;
this.unit = unit_override;
}

const isCharging = getChargingState(this.config, this.state, this.hass);
Expand Down
2 changes: 2 additions & 0 deletions src/custom-elements/battery-state-entity.views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const replaceTags = (text: string, hass?: HomeAssistant): TemplateResult[] => {
result.push(html`${text.substring(currentPos, matchPos)}`);
}

console.log(matches);

result.push(html`<ha-relative-time .hass="${hass}" .datetime="${new Date(matches[1])}"></ha-relative-time>`);

currentPos += matchPos + matches[0].length;
Expand Down
14 changes: 7 additions & 7 deletions src/custom-elements/lovelace-card.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { HomeAssistant } from "custom-card-helpers";
import { LitElement, TemplateResult } from "lit";
import { HomeAssistantExt } from "../type-extensions";
import { throttledCall } from "../utils";

/**
Expand All @@ -10,7 +10,7 @@ export abstract class LovelaceCard<TConfig> extends LitElement {
/**
* HomeAssistant object
*/
private _hass: HomeAssistant | undefined;
private _hass: HomeAssistantExt | undefined;

/**
* Component/card config
Expand All @@ -34,9 +34,9 @@ export abstract class LovelaceCard<TConfig> extends LitElement {

/**
* Safe update triggering function
*
* It happens quite often that setConfig or hassio setter are called few times
* in the same execution path. We want to throttle such updates and handle just
*
* It happens quite often that setConfig or hassio setter are called few times
* in the same execution path. We want to throttle such updates and handle just
* the last one.
*/
private triggerUpdate = throttledCall(async () => {
Expand All @@ -50,7 +50,7 @@ export abstract class LovelaceCard<TConfig> extends LitElement {
/**
* HomeAssistant object setter
*/
set hass(hass: HomeAssistant | undefined) {
set hass(hass: HomeAssistantExt | undefined) {
this._hass = hass;
this.hassUpdated = true;
this.triggerUpdate();
Expand All @@ -59,7 +59,7 @@ export abstract class LovelaceCard<TConfig> extends LitElement {
/**
* HomeAssistant object getter
*/
get hass(): HomeAssistant | undefined {
get hass(): HomeAssistantExt | undefined {
return this._hass;
}

Expand Down
24 changes: 20 additions & 4 deletions src/entity-fields/battery-level.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { HomeAssistant } from "custom-card-helpers/dist/types";
import { RichStringProcessor } from "../rich-string-processor";
import { HomeAssistantExt } from "../type-extensions";
import { isNumber, log } from "../utils";

/**
* Some sensor may produce string value like "45%". This regex is meant to parse such values.
*/
const stringValuePattern = /\b([0-9]{1,3})\s?%/;
const stringValuePattern = /\b([0-9]{1,3})\s?%/;

/**
* Getts battery level/state
* @param config Entity config
* @param hass HomeAssistant state object
* @returns Battery level
*/
export const getBatteryLevel = (config: IBatteryEntityConfig, hass?: HomeAssistant): IBatteryState => {
export const getBatteryLevel = (config: IBatteryEntityConfig, hass?: HomeAssistantExt): IBatteryState => {
const UnknownLevel = hass?.localize("state.default.unknown") || "Unknown";
let state: string;
let unit: string | undefined;

const stringProcessor = new RichStringProcessor(hass, config.entity);

Expand Down Expand Up @@ -94,9 +95,19 @@ import { isNumber, log } from "../utils";
state = state.charAt(0).toUpperCase() + state.slice(1);
}

// check if HA should format the value
if (config.default_state_formatting !== false && !displayValue && state === entityData.state) {
const formattedState = hass.formatEntityState(entityData);

// assuming it is a number followed by unit
[displayValue, unit] = formattedState.split(" ", 2);
unit = String.fromCharCode(160) + unit;
}

return {
state: displayValue || state,
level: isNumber(state) ? Number(state) : undefined
level: isNumber(state) ? Number(state) : undefined,
unit_override: unit,
};
}

Expand All @@ -110,4 +121,9 @@ interface IBatteryState {
* Battery state to display
*/
state: string;

/**
* Unit override
*/
unit_override?: string
}
14 changes: 14 additions & 0 deletions src/type-extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { HomeAssistant } from "custom-card-helpers";

/**
* https://github.com/home-assistant/frontend/blob/dev/src/types.ts
*/
export interface HomeAssistantExt extends HomeAssistant {
formatEntityState(stateObj: any, state?: string): string;
formatEntityAttributeValue(
stateObj: any,
attribute: string,
value?: any
): string;
formatEntityAttributeName(stateObj: any, attribute: string): string;
}
5 changes: 5 additions & 0 deletions src/typings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,11 @@ interface IBatteryEntityConfig {
* Whether the entity is not a battery entity
*/
non_battery_entity?: boolean;

/**
* Whether to allow HA to format the state value
*/
default_state_formatting?: boolean;
}

interface IBatteryCardConfig {
Expand Down
11 changes: 8 additions & 3 deletions test/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HomeAssistant } from "custom-card-helpers";
import { BatteryStateCard } from "../src/custom-elements/battery-state-card";
import { BatteryStateEntity } from "../src/custom-elements/battery-state-entity";
import { LovelaceCard } from "../src/custom-elements/lovelace-card";
import { HomeAssistantExt } from "../src/type-extensions";
import { throttledCall } from "../src/utils";

/**
Expand Down Expand Up @@ -70,9 +70,10 @@ export class HomeAssistantMock<T extends LovelaceCard<any>> {

private cards: LovelaceCard<any>[] = [];

public hass: HomeAssistant = <any>{
public hass: HomeAssistantExt = <any>{
states: {},
localize: jest.fn((key: string) => `[${key}]`)
localize: jest.fn((key: string) => `[${key}]`),
formatEntityState: jest.fn((entityData: any) => `${entityData.state} %`),
};

private throttledUpdate = throttledCall(() => {
Expand All @@ -85,6 +86,10 @@ export class HomeAssistantMock<T extends LovelaceCard<any>> {
}
}

mockFunc(funcName: keyof HomeAssistantExt, mockedFunc: Function) {
(<any>this.hass)[funcName] = jest.fn(<any>mockedFunc)
}

addCard<K extends LovelaceCard<T>>(type: string, config: extractGeneric<T>): T {
const elementName = type.replace("custom:", "");

Expand Down
20 changes: 20 additions & 0 deletions test/other/entity-fields/battery-level.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,24 @@ describe("Battery level", () => {
expect(level).toBe(expectedLevel);
expect(state).toBe(expectedState);
});

test.each([
[undefined, "45", "dbm", { state: "[45]", level: 45, unit_override: String.fromCharCode(160) + "[dbm]" }], // test default when the setting is not set in the config
[true, "45", "dbm", { state: "[45]", level: 45, unit_override: String.fromCharCode(160) + "[dbm]" }], // test when the setting is explicitly true
[false, "45", "dbm", { state: "45", level: 45, unit_override: undefined }], // test when the setting is turned off
[true, "45", "dbm", { state: "56", level: 56, unit_override: undefined }, [ { from: "45", to: "56" } ]], // test when the state was changed by state_map
[true, "45", "dbm", { state: "33", level: 45, unit_override: undefined }, [ { from: "45", to: "45", display: "33" } ]], // test when the display value was changed by state_map
])
("default HA formatting ", (defaultStateFormatting: boolean | undefined, entityState: string, unitOfMeasurement: string, expected: { state: string, level: number, unit_override?: string }, stateMap: IConvert[] | undefined = undefined) => {

const hassMock = new HomeAssistantMock(true);
hassMock.addEntity("Mocked entity", entityState);
hassMock.mockFunc("formatEntityState", (entityData: any) => `[${entityData.state}] [${unitOfMeasurement}]`);

const { state, level, unit_override } = getBatteryLevel({ entity: "mocked_entity", default_state_formatting: defaultStateFormatting, state_map: stateMap }, hassMock.hass);

expect(level).toBe(expected.level);
expect(state).toBe(expected.state);
expect(unit_override).toBe(expected.unit_override);
});
});
47 changes: 44 additions & 3 deletions test/other/entity-fields/charging-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ describe("Charging state", () => {
const hassMock = new HomeAssistantMock(true);
const isCharging = getChargingState({ entity: "any" }, "90", hassMock.hass);

expect(isCharging).toBe(false);
})

test("is false when there is no hass", () => {
const isCharging = getChargingState(
{ entity: "sensor.my_entity", charging_state: { attribute: [ { name: "is_charging", value: "true" } ] } },
"45",
undefined);

expect(isCharging).toBe(false);
})

Expand Down Expand Up @@ -44,15 +53,47 @@ describe("Charging state", () => {
expect(isCharging).toBe(true);
})

test("is true when charging state is in the external entity state", () => {
test("is false when charging state is in attribute (and attribute is missing)", () => {
const hassMock = new HomeAssistantMock(true);
const entity = hassMock.addEntity("Sensor", "80", { is_charging: "true" })
const entity = hassMock.addEntity("Sensor", "80")
const isCharging = getChargingState(
{ entity: entity.entity_id, charging_state: { attribute: [ { name: "status", value: "charging" }, { name: "is_charging", value: "true" } ] } },
entity.state,
hassMock.hass);

expect(isCharging).toBe(true);
expect(isCharging).toBe(false);
})

test.each([
["charging", true],
["charging", false, "MissingEntity"],
["discharging", false]
])("charging state is in the external entity state", (chargingEntityState: string, expected: boolean, missingEntitySuffix = "") => {
const hassMock = new HomeAssistantMock(true);
const entity = hassMock.addEntity("Sensor", "80")
const entityChargingState = hassMock.addEntity("Charging sensor", chargingEntityState)
const isCharging = getChargingState(
{ entity: entity.entity_id, charging_state: { entity_id: entityChargingState.entity_id + missingEntitySuffix, state: "charging" } },
entity.state,
hassMock.hass);

expect(isCharging).toBe(expected);
})

test.each([
["charging", true],
["full", true],
["full", false, " missing"],
])("default charging state", (chargingEntityState: string, expected: boolean, missingEntitySuffix = "") => {
const hassMock = new HomeAssistantMock(true);
const entity = hassMock.addEntity("Sensor battery level", "80", { is_charging: "true" })
const entityChargingState = hassMock.addEntity("Sensor battery state" + missingEntitySuffix, chargingEntityState)
const isCharging = getChargingState(
{ entity: entity.entity_id },
entity.state,
hassMock.hass);

expect(isCharging).toBe(expected);
})

});
Loading