diff --git a/package.json b/package.json index dba488c..c96a15c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,9 @@ "release": "rollup --environment RELEASE -c", "watch": "rollup -c --watch", "test": "jest", - "test+coverage": "jest --coverage --testPathPattern=test/other", + "test+integration": "jest --testPathPattern=test/entity", + "test+coverage": "jest --coverage", + "test+coverage+unit": "jest --coverage --testPathPattern=test/other", "test+debug": "SET DEBUG_MODE=1&&jest" }, "jest": { diff --git a/src/custom-elements/battery-state-entity.ts b/src/custom-elements/battery-state-entity.ts index 9f346a4..e799e68 100644 --- a/src/custom-elements/battery-state-entity.ts +++ b/src/custom-elements/battery-state-entity.ts @@ -79,16 +79,10 @@ export class BatteryStateEntity extends LovelaceCard { }; this.name = getName(this.config, this.hass); - var { state, level, unit_override} = getBatteryLevel(this.config, this.hass); + var { state, level, unit} = getBatteryLevel(this.config, this.hass); this.state = state; - - 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 = unit_override; - } - + this.unit = unit; + const isCharging = getChargingState(this.config, this.state, this.hass); this.secondaryInfo = getSecondaryInfo(this.config, this.hass, isCharging); this.icon = getIcon(this.config, level, isCharging, this.hass); diff --git a/src/custom-elements/battery-state-entity.views.ts b/src/custom-elements/battery-state-entity.views.ts index 7a0e5f2..28adb52 100644 --- a/src/custom-elements/battery-state-entity.views.ts +++ b/src/custom-elements/battery-state-entity.views.ts @@ -24,8 +24,6 @@ const replaceTags = (text: string, hass?: HomeAssistant): TemplateResult[] => { result.push(html`${text.substring(currentPos, matchPos)}`); } - console.log(matches); - result.push(html``); currentPos += matchPos + matches[0].length; @@ -58,6 +56,8 @@ ${icon(model.icon, model.iconColor)} ${secondaryInfo(model.secondaryInfo, model.hass)}
- ${model.state}${model.unit} + ${model.state}${unit(model.unit)}
-`; \ No newline at end of file +`; + +const unit = (unit: string | undefined) => unit && html` ${unit}`; \ No newline at end of file diff --git a/src/entity-fields/battery-level.ts b/src/entity-fields/battery-level.ts index 8c6ff25..2104b60 100644 --- a/src/entity-fields/battery-level.ts +++ b/src/entity-fields/battery-level.ts @@ -24,7 +24,8 @@ export const getBatteryLevel = (config: IBatteryEntityConfig, hass?: HomeAssista const processedValue = stringProcessor.process(config.value_override.toString()); return { state: processedValue, - level: isNumber(processedValue) ? Number(processedValue) : undefined + level: isNumber(processedValue) ? Number(processedValue) : undefined, + unit: getUnit(processedValue, undefined, config, hass), } } @@ -101,16 +102,34 @@ export const getBatteryLevel = (config: IBatteryEntityConfig, hass?: HomeAssista // assuming it is a number followed by unit [displayValue, unit] = formattedState.split(" ", 2); - unit = String.fromCharCode(160) + unit; + unit = unit; } return { state: displayValue || state, level: isNumber(state) ? Number(state) : undefined, - unit_override: unit, + unit: getUnit(state, unit, config, hass), }; } +const getUnit = (state: string, unit: string | undefined, config: IBatteryEntityConfig, hass?: HomeAssistantExt): string | undefined => { + if (config.unit) { + // config unit override + unit = config.unit + } + else { + // default unit + unit = unit || hass?.states[config.entity]?.attributes["unit_of_measurement"] || "%" + } + + if (!isNumber(state)) { + // for non numeric states unit should not be rendered + unit = undefined; + } + + return unit; +} + interface IBatteryState { /** * Battery level @@ -125,5 +144,5 @@ interface IBatteryState { /** * Unit override */ - unit_override?: string + unit?: string } \ No newline at end of file diff --git a/src/entity-fields/get-secondary-info.ts b/src/entity-fields/get-secondary-info.ts index 97bc01b..c24271d 100644 --- a/src/entity-fields/get-secondary-info.ts +++ b/src/entity-fields/get-secondary-info.ts @@ -25,7 +25,7 @@ export const getSecondaryInfo = (config: IBatteryEntityConfig, hass: HomeAssista const dateVal = Date.parse(result); // The RT tags will be converted to proper HA tags at the views layer - return isNaN(dateVal) ? result : "" + new Date(dateVal).getTime() + ""; + return isNaN(dateVal) ? result : `${result}`; } return null; diff --git a/test/entity/secondary-info.test.ts b/test/entity/secondary-info.test.ts index b346a49..0714856 100644 --- a/test/entity/secondary-info.test.ts +++ b/test/entity/secondary-info.test.ts @@ -56,8 +56,8 @@ test("Secondary info date value - renders relative time element", async () => { const hass = new HomeAssistantMock(); const flowerBattery = hass.addEntity("Flower sensor battery level", "80", {}); - let dateString = JSON.stringify(new Date(2022, 1, 24, 23, 45, 55)); - dateString = dateString.substring(1, dateString.length - 1); // removing quotes + let dateStringSerialized = JSON.stringify(new Date(2022, 1, 24, 23, 45, 55)); + const dateString = dateStringSerialized.substring(1, dateStringSerialized.length - 1); // removing quotes flowerBattery.setLastUpdated(dateString); const cardElem = hass.addCard("battery-state-entity", { @@ -68,5 +68,29 @@ test("Secondary info date value - renders relative time element", async () => { await cardElem.cardUpdated; const entity = new EntityElements(cardElem); - expect((entity.secondaryInfo?.firstElementChild).tagName).toBe("HA-RELATIVE-TIME"); -}); \ No newline at end of file + const relTimeElem = entity.secondaryInfo?.firstElementChild; + expect(relTimeElem.tagName).toBe("HA-RELATIVE-TIME"); + expect(JSON.stringify((relTimeElem).datetime)).toBe(dateStringSerialized); +}); + +// test("Secondary info date value - renders relative time element with text", async () => { +// const hass = new HomeAssistantMock(); +// const flowerBattery = hass.addEntity("Flower sensor battery level", "80", {}); + +// const date = new Date(2022, 1, 24, 23, 45, 55); +// let dateString = JSON.stringify(date); +// dateString = dateString.substring(1, dateString.length - 1); // removing quotes +// flowerBattery.setLastUpdated(dateString); + +// const cardElem = hass.addCard("battery-state-entity", { +// entity: flowerBattery.entity_id, +// secondary_info: "Last updated: {last_updated}", +// }); + +// await cardElem.cardUpdated; + +// const entity = new EntityElements(cardElem); +// const relTimeElem = entity.secondaryInfo?.firstElementChild; +// expect(relTimeElem.tagName).toBe("HA-RELATIVE-TIME"); +// expect((relTimeElem).datetime).toBe(date); +// }); \ No newline at end of file diff --git a/test/other/entity-fields/battery-level.test.ts b/test/other/entity-fields/battery-level.test.ts index 95239c1..06acc23 100644 --- a/test/other/entity-fields/battery-level.test.ts +++ b/test/other/entity-fields/battery-level.test.ts @@ -5,27 +5,30 @@ describe("Battery level", () => { test("is equal value_override setting when it is provided", () => { const hassMock = new HomeAssistantMock(true); - const { state, level } = getBatteryLevel({ entity: "any", value_override: "45" }, hassMock.hass); + const { state, level, unit } = getBatteryLevel({ entity: "any", value_override: "45" }, hassMock.hass); expect(level).toBe(45); expect(state).toBe("45"); + expect(unit).toBe("%"); }); test("is 'Unknown' when entity not found and no localized string", () => { const hassMock = new HomeAssistantMock(true); hassMock.hass.localize = () => null; - const { state, level } = getBatteryLevel({ entity: "any" }, hassMock.hass); + const { state, level, unit } = getBatteryLevel({ entity: "any" }, hassMock.hass); - expect(level).toBeUndefined() + expect(level).toBeUndefined(); expect(state).toBe("Unknown"); + expect(unit).toBeUndefined(); }); test("is 'Unknown' localized string when entity not found", () => { const hassMock = new HomeAssistantMock(true); - const { state, level } = getBatteryLevel({ entity: "any" }, hassMock.hass); + const { state, level, unit } = getBatteryLevel({ entity: "any" }, hassMock.hass); - expect(level).toBeUndefined() + expect(level).toBeUndefined(); expect(state).toBe("[state.default.unknown]"); + expect(unit).toBeUndefined(); }); test("is taken from attribute but attribute is missing", () => { @@ -33,10 +36,11 @@ describe("Battery level", () => { const hassMock = new HomeAssistantMock(true); hassMock.addEntity("Mocked entity", "OK", { battery_state: "45" }); - const { state, level } = getBatteryLevel({ entity: "mocked_entity", attribute: "battery_state_missing" }, hassMock.hass); + const { state, level, unit } = getBatteryLevel({ entity: "mocked_entity", attribute: "battery_state_missing" }, hassMock.hass); - expect(level).toBeUndefined() + expect(level).toBeUndefined(); expect(state).toBe("[state.default.unknown]"); + expect(unit).toBeUndefined(); }); test("is taken from attribute", () => { @@ -44,10 +48,11 @@ describe("Battery level", () => { const hassMock = new HomeAssistantMock(true); hassMock.addEntity("Mocked entity", "OK", { battery_state: "45" }); - const { state, level } = getBatteryLevel({ entity: "mocked_entity", attribute: "battery_state" }, hassMock.hass); + const { state, level, unit } = getBatteryLevel({ entity: "mocked_entity", attribute: "battery_state" }, hassMock.hass); expect(level).toBe(45); expect(state).toBe("45"); + expect(unit).toBe("%"); }); test("is taken from attribute - value includes percentage", () => { @@ -164,41 +169,70 @@ describe("Battery level", () => { }); test.each([ - ["ok", "100", 100, undefined], - ["empty", "0", 0, undefined], - ["20", "20", 20, undefined], - ["charge", "Empty", 0, "Empty"], - ["charge", "StateFromOtherEntity", 0, "{sensor.other_entity.state}"], + ["ok", "100", 100, "%", undefined], + ["empty", "0", 0, "%", undefined], + ["20", "20", 20, "%", undefined], + ["charge", "Empty", 0, "%", "Empty"], + ["charge", "StateFromOtherEntity", 0, "%", "{sensor.other_entity.state}"], ]) - ("state map applied", (entityState: string, expectedState: string, expectedLevel: number | undefined, display?: string) => { + ("state map applied", (entityState: string, expectedState: string, expectedLevel: number | undefined, expectedUnit: string | undefined, display?: string) => { const hassMock = new HomeAssistantMock(true); hassMock.addEntity("Mocked entity", entityState); hassMock.addEntity("Other entity", "StateFromOtherEntity", undefined, "sensor"); - const { state, level } = getBatteryLevel({ entity: "mocked_entity", state_map: [ { from: "ok", to: "100" }, { from: "empty", to: "0" }, { from: "charge", to: "0", display } ] }, hassMock.hass); + const { state, level, unit } = getBatteryLevel({ entity: "mocked_entity", state_map: [ { from: "ok", to: "100" }, { from: "empty", to: "0" }, { from: "charge", to: "0", display } ] }, hassMock.hass); expect(level).toBe(expectedLevel); expect(state).toBe(expectedState); + expect(unit).toBe(expectedUnit); }); 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 + [undefined, "45", "dbm", { state: "[45]", level: 45, unit: "[dbm]" }], // test default when the setting is not set in the config + [true, "45", "dbm", { state: "[45]", level: 45, unit: "[dbm]" }], // test when the setting is explicitly true + [false, "45", "dbm", { state: "45", level: 45, unit: "%" }], // test when the setting is turned off + [true, "45", "dbm", { state: "56", level: 56, unit: "%" }, [ { from: "45", to: "56" } ]], // test when the state was changed by state_map + [true, "45", "dbm", { state: "33", level: 45, unit: "%" }, [ { 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) => { + ("default HA formatting ", (defaultStateFormatting: boolean | undefined, entityState: string, unitOfMeasurement: string, expected: { state: string, level: number, unit?: 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); + const { state, level, unit } = 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); + expect(unit).toBe(expected.unit); }); + + test.each([ + ["OK", undefined, undefined, undefined], + ["45", undefined, undefined, "%"], + ["45", "dBm", undefined, "dBm"], + ["45", "dBm", "rpm", "rpm"], + ])("unit is correct", (entityState: string, entityUnitOfMeasurement: string | undefined, configOverride: string | undefined, expectedUnit: string | undefined) => { + const hassMock = new HomeAssistantMock(true); + const entity = hassMock.addEntity("Mocked entity", entityState, { unit_of_measurement: entityUnitOfMeasurement }); + + const { unit } = getBatteryLevel({ entity: entity.entity_id, default_state_formatting: false, unit: configOverride }, hassMock.hass); + + expect(unit).toBe(expectedUnit); + }) + + test.each([ + ["OK", undefined, undefined, undefined], + ["45", undefined, undefined, "%"], + ["45", "dBm", undefined, "dBm"], + ["45", "dBm", "rpm", "rpm"], + ])("unit is correct when value_override is used", (entityState: string, entityUnitOfMeasurement: string | undefined, configOverride: string | undefined, expectedUnit: string | undefined) => { + const hassMock = new HomeAssistantMock(true); + const entity = hassMock.addEntity("Mocked entity", entityState, { unit_of_measurement: entityUnitOfMeasurement }); + + const { unit } = getBatteryLevel({ entity: entity.entity_id, default_state_formatting: false, unit: configOverride, value_override: "{state}" }, hassMock.hass); + + expect(unit).toBe(expectedUnit); + }) }); \ No newline at end of file diff --git a/test/other/entity-fields/get-secondary-info.test.ts b/test/other/entity-fields/get-secondary-info.test.ts index cbcee32..4952e96 100644 --- a/test/other/entity-fields/get-secondary-info.test.ts +++ b/test/other/entity-fields/get-secondary-info.test.ts @@ -28,7 +28,7 @@ describe("Secondary info", () => { const secondaryInfoConfig = "{last_changed}"; const result = getSecondaryInfo({ entity: entity.entity_id, secondary_info: secondaryInfoConfig }, hassMock.hass, false); - expect(result).toBe("1644192000000"); + expect(result).toBe("2022-02-07"); }) test("Secondary info config not set'", () => {