Skip to content

Commit

Permalink
Merge pull request #619 from maxwroc/FormatState
Browse files Browse the repository at this point in the history
Use default home assistant state formatting
  • Loading branch information
maxwroc authored Dec 29, 2023
2 parents ac8f41e + a841fae commit 8a6cba0
Show file tree
Hide file tree
Showing 12 changed files with 328 additions and 221 deletions.
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);
})

});

0 comments on commit 8a6cba0

Please sign in to comment.