From 9248075d19c90dc360b6e4320e41e1c6e7d4a9ea Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Mon, 2 May 2022 20:28:14 -0700 Subject: [PATCH] Import full set of entity icons. --- src/common.ts | 6 +- src/icons/LICENSE | 201 ++++++++++++++++++++++++++++++++ src/icons/README.md | 7 ++ src/icons/alarm-panel-icon.ts | 42 +++++++ src/icons/binary-sensor-icon.ts | 58 +++++++++ src/icons/cover-icon.ts | 113 ++++++++++++++++++ src/icons/domain-icon.ts | 166 ++++++++++++++++++++++++++ src/icons/sensor-icon.ts | 112 ++++++++++++++++++ src/icons/state-icon.ts | 18 +++ src/icons/update.ts | 32 +++++ 10 files changed, 752 insertions(+), 3 deletions(-) create mode 100644 src/icons/LICENSE create mode 100644 src/icons/README.md create mode 100644 src/icons/alarm-panel-icon.ts create mode 100644 src/icons/binary-sensor-icon.ts create mode 100644 src/icons/cover-icon.ts create mode 100644 src/icons/domain-icon.ts create mode 100644 src/icons/sensor-icon.ts create mode 100644 src/icons/state-icon.ts create mode 100644 src/icons/update.ts diff --git a/src/common.ts b/src/common.ts index 75a3c70d..42a50576 100644 --- a/src/common.ts +++ b/src/common.ts @@ -4,7 +4,6 @@ import { computeStateDomain, handleActionConfig, hasAction, - stateIcon, ActionConfig, } from 'custom-card-helpers'; import { StyleInfo } from 'lit/directives/style-map.js'; @@ -34,6 +33,7 @@ import { signedPathSchema, StateParameters, } from './types.js'; +import { stateIcon } from './icons/state-icon.js' const MEDIA_INFO_HEIGHT_CUTOFF = 50; const MEDIA_INFO_WIDTH_CUTOFF = MEDIA_INFO_HEIGHT_CUTOFF; @@ -476,8 +476,8 @@ export function getEntityTitle( export function getEntityIcon( hass?: HomeAssistant, entity?: string, -): string | undefined { - return hass && entity ? stateIcon(hass.states[entity]) : undefined; +): string { + return stateIcon(entity ? hass?.states[entity] : null); } /** diff --git a/src/icons/LICENSE b/src/icons/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/src/icons/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/icons/README.md b/src/icons/README.md new file mode 100644 index 00000000..89ca8032 --- /dev/null +++ b/src/icons/README.md @@ -0,0 +1,7 @@ +## Icons + +These entity icon mappings are copied (with slight tweaks to work with this +card), from the [Lovelace Mushroom +Card](https://github.com/piitaya/lovelace-mushroom/tree/main/src/utils/icons), which are in turn copied from [the Home Assistant frontend](https://github.com/home-assistant/frontend/tree/dev/src/common/entity) in order to +replace the out of date entity icons in +[custom-card-helpers](https://github.com/custom-cards/custom-card-helpers). \ No newline at end of file diff --git a/src/icons/alarm-panel-icon.ts b/src/icons/alarm-panel-icon.ts new file mode 100644 index 00000000..8f85affa --- /dev/null +++ b/src/icons/alarm-panel-icon.ts @@ -0,0 +1,42 @@ +export const alarmPanelIcon = (state?: string) => { + switch (state) { + case "armed_away": + return "mdi:shield-lock"; + case "armed_vacation": + return "mdi:shield-airplane"; + case "armed_home": + return "mdi:shield-home"; + case "armed_night": + return "mdi:shield-moon"; + case "armed_custom_bypass": + return "mdi:security"; + case "pending": + case "arming": + return "mdi:shield-sync"; + case "triggered": + return "mdi:bell-ring"; + case "disarmed": + return "mdi:shield-off"; + default: + return "mdi:shield"; + } +}; + +export const alarmPanelIconAction = (state?: string) => { + switch (state) { + case "armed_away": + return "mdi:shield-lock-outline"; + case "armed_vacation": + return "mdi:shield-airplane-outline"; + case "armed_home": + return "mdi:shield-home-outline"; + case "armed_night": + return "mdi:shield-moon-outline"; + case "armed_custom_bypass": + return "mdi:shield-half-full"; + case "disarmed": + return "mdi:shield-off-outline"; + default: + return "mdi:shield-outline"; + } +}; diff --git a/src/icons/binary-sensor-icon.ts b/src/icons/binary-sensor-icon.ts new file mode 100644 index 00000000..c97c198e --- /dev/null +++ b/src/icons/binary-sensor-icon.ts @@ -0,0 +1,58 @@ +import { HassEntity } from "home-assistant-js-websocket"; + +export const binarySensorIcon = (state?: string, entity?: HassEntity) => { + const isOff = state === "off"; + switch (entity?.attributes.device_class) { + case "battery": + return isOff ? "mdi:battery" : "mdi:battery-outline"; + case "battery_charging": + return isOff ? "mdi:battery" : "mdi:battery-charging"; + case "cold": + return isOff ? "mdi:thermometer" : "mdi:snowflake"; + case "connectivity": + return isOff ? "mdi:close-network-outline" : "mdi:check-network-outline"; + case "door": + return isOff ? "mdi:door-closed" : "mdi:door-open"; + case "garage_door": + return isOff ? "mdi:garage" : "mdi:garage-open"; + case "power": + return isOff ? "mdi:power-plug-off" : "mdi:power-plug"; + case "gas": + case "problem": + case "safety": + case "tamper": + return isOff ? "mdi:check-circle" : "mdi:alert-circle"; + case "smoke": + return isOff ? "mdi:check-circle" : "mdi:smoke"; + case "heat": + return isOff ? "mdi:thermometer" : "mdi:fire"; + case "light": + return isOff ? "mdi:brightness5" : "mdi:brightness-7"; + case "lock": + return isOff ? "mdi:lock" : "mdi:lock-open"; + case "moisture": + return isOff ? "mdi:water-off" : "mdi:water"; + case "motion": + return isOff ? "mdi:motion-sensor-off" : "mdi:motion-sensor"; + case "occupancy": + return isOff ? "mdi:home-outline" : "mdi:home"; + case "opening": + return isOff ? "mdi:square" : "mdi:square-outline"; + case "plug": + return isOff ? "mdi:power-plug-off" : "mdi:power-plug"; + case "presence": + return isOff ? "mdi:home-outline" : "mdi:home"; + case "running": + return isOff ? "mdi:stop" : "mdi:play"; + case "sound": + return isOff ? "mdi:music-note-off" : "mdi:music-note"; + case "update": + return isOff ? "mdi:package" : "mdi:package-up"; + case "vibration": + return isOff ? "mdi:crop-portrait" : "mdi:vibrate"; + case "window": + return isOff ? "mdi:window-closed" : "mdi:window-open"; + default: + return isOff ? "mdi:radiobox-blank" : "mdi:checkbox-marked-circle"; + } +}; diff --git a/src/icons/cover-icon.ts b/src/icons/cover-icon.ts new file mode 100644 index 00000000..db3ff716 --- /dev/null +++ b/src/icons/cover-icon.ts @@ -0,0 +1,113 @@ +import { HassEntity } from "home-assistant-js-websocket"; + +export const coverIcon = (state?: string, entity?: HassEntity): string => { + const open = state !== "closed"; + + switch (entity?.attributes.device_class) { + case "garage": + switch (state) { + case "opening": + return "mdi:arrow-up-box"; + case "closing": + return "mdi:arrow-down-box"; + case "closed": + return "mdi:garage"; + default: + return "mdi:garage-open"; + } + case "gate": + switch (state) { + case "opening": + case "closing": + return "mdi:gate-arrow-right"; + case "closed": + return "mdi:gate"; + default: + return "mdi:gate-open"; + } + case "door": + return open ? "mdi:door-open" : "mdi:door-closed"; + case "damper": + return open ? "md:circle" : "mdi:circle-slice-8"; + case "shutter": + switch (state) { + case "opening": + return "mdi:arrow-up-box"; + case "closing": + return "mdi:arrow-down-box"; + case "closed": + return "mdi:window-shutter"; + default: + return "mdi:window-shutter-open"; + } + case "curtain": + switch (state) { + case "opening": + return "mdi:arrow-split-vertical"; + case "closing": + return "mdi:arrow-collapse-horizontal"; + case "closed": + return "mdi:curtains-closed"; + default: + return "mdi:curtains"; + } + case "blind": + case "shade": + switch (state) { + case "opening": + return "mdi:arrow-up-box"; + case "closing": + return "mdi:arrow-down-box"; + case "closed": + return "mdi:blinds"; + default: + return "mdi:blinds-open"; + } + case "window": + switch (state) { + case "opening": + return "mdi:arrow-up-box"; + case "closing": + return "mdi:arrow-down-box"; + case "closed": + return "mdi:window-closed"; + default: + return "mdi:window-open"; + } + } + + switch (state) { + case "opening": + return "mdi:arrow-up-box"; + case "closing": + return "mdi:arrow-down-box"; + case "closed": + return "mdi:window-closed"; + default: + return "mdi:window-open"; + } +}; + +export const computeOpenIcon = (stateObj: HassEntity): string => { + switch (stateObj.attributes.device_class) { + case "awning": + case "curtain": + case "door": + case "gate": + return "mdi:arrow-expand-horizontal"; + default: + return "mdi:arrow-up"; + } +}; + +export const computeCloseIcon = (stateObj: HassEntity): string => { + switch (stateObj.attributes.device_class) { + case "awning": + case "curtain": + case "door": + case "gate": + return "mdi:arrow-collapse-horizontal"; + default: + return "mdi:arrow-down"; + } +}; diff --git a/src/icons/domain-icon.ts b/src/icons/domain-icon.ts new file mode 100644 index 00000000..3a6f4e25 --- /dev/null +++ b/src/icons/domain-icon.ts @@ -0,0 +1,166 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { UpdateEntity, updateIsInstalling } from "./update"; +import { alarmPanelIcon } from "./alarm-panel-icon"; +import { binarySensorIcon } from "./binary-sensor-icon"; +import { coverIcon } from "./cover-icon"; +import { sensorIcon } from "./sensor-icon"; + +export const DEFAULT_DOMAIN_ICON = "mdi:bookmark"; + +const FIXED_DOMAIN_ICONS = { + alert: "mdi:alert", + air_quality: "mdi:air-filter", + automation: "mdi:robot", + calendar: "mdi:calendar", + camera: "mdi:video", + climate: "mdi:thermostat", + configurator: "mdi:cog", + conversation: "mdi:text-to-speech", + counter: "mdi:counter", + fan: "mdi:fan", + google_assistant: "mdi:google-assistant", + group: "mdi:google-circles-communities", + homeassistant: "mdi:home-assistant", + homekit: "mdi:home-automation", + image_processing: "mdi:image-filter-frames", + input_button: "mdi:gesture-tap-button", + input_datetime: "mdi:calendar-clock", + input_number: "mdi:ray-vertex", + input_select: "mdi:format-list-bulleted", + input_text: "mdi:form-textbox", + light: "mdi:lightbulb", + mailbox: "mdi:mailbox", + notify: "mdi:comment-alert", + number: "mdi:ray-vertex", + persistent_notification: "mdi:bell", + person: "mdi:account", + plant: "mdi:flower", + proximity: "mdi:apple-safari", + remote: "mdi:remote", + scene: "mdi:palette", + script: "mdi:script-text", + select: "mdi:format-list-bulleted", + sensor: "mdi:eye", + siren: "mdi:bullhorn", + simple_alarm: "mdi:bell", + sun: "mdi:white-balance-sunny", + timer: "mdi:timer-outline", + updater: "mdi:cloud-upload", + vacuum: "mdi:robot-vacuum", + water_heater: "mdi:thermometer", + weather: "mdi:weather-cloudy", + zone: "mdi:map-marker-radius", +}; + +export function domainIcon(domain: string, entity?: HassEntity, state?: string): string { + switch (domain) { + case "alarm_control_panel": + return alarmPanelIcon(state); + + case "binary_sensor": + return binarySensorIcon(state, entity); + + case "button": + switch (entity?.attributes.device_class) { + case "restart": + return "mdi:restart"; + case "update": + return "mdi:package-up"; + default: + return "mdi:gesture-tap-button"; + } + + case "cover": + return coverIcon(state, entity); + + case "device_tracker": + if (entity?.attributes.source_type === "router") { + return state === "home" ? "mdi:lan-connect" : "mdi:lan-disconnect"; + } + if (["bluetooth", "bluetooth_le"].includes(entity?.attributes.source_type)) { + return state === "home" ? "mdi:bluetooth-connect" : "mdi:bluetooth"; + } + return state === "not_home" ? "mdi:account-arrow-right" : "mdi:account"; + + case "humidifier": + return state && state === "off" ? "mdi:air-humidifier-off" : "mdi:air-humidifier"; + + case "input_boolean": + return state === "on" ? "mdi:check-circle-outline" : "mdi:close-circle-outline"; + + case "lock": + switch (state) { + case "unlocked": + return "mdi:lock-open"; + case "jammed": + return "mdi:lock-alert"; + case "locking": + case "unlocking": + return "mdi:lock-clock"; + default: + return "mdi:lock"; + } + + case "media_player": + return state === "playing" ? "mdi:cast-connected" : "mdi:cast"; + + case "switch": + switch (entity?.attributes.device_class) { + case "outlet": + return state === "on" ? "mdi:power-plug" : "mdi:power-plug-off"; + case "switch": + return state === "on" ? "mdi:toggle-switch" : "mdi:toggle-switch-off"; + default: + return "mdi:flash"; + } + + case "zwave": + switch (state) { + case "dead": + return "mdi:emoticon-dead"; + case "sleeping": + return "mdi:sleep"; + case "initializing": + return "mdi:timer-sand"; + default: + return "mdi:z-wave"; + } + + case "sensor": { + const icon = sensorIcon(entity); + if (icon) { + return icon; + } + + break; + } + + case "input_datetime": + if (!entity?.attributes.has_date) { + return "mdi:clock"; + } + if (!entity.attributes.has_time) { + return "mdi:calendar"; + } + break; + + case "sun": + return entity?.state === "above_horizon" + ? FIXED_DOMAIN_ICONS[domain] + : "mdi:weather-night"; + + case "update": + return entity?.state === "on" + ? updateIsInstalling(entity as UpdateEntity) + ? "mdi:package-down" + : "mdi:package-up" + : "mdi:package"; + } + + if (domain in FIXED_DOMAIN_ICONS) { + return FIXED_DOMAIN_ICONS[domain]; + } + + console.warn(`Unable to find icon for domain ${domain}`); + return DEFAULT_DOMAIN_ICON; +} diff --git a/src/icons/sensor-icon.ts b/src/icons/sensor-icon.ts new file mode 100644 index 00000000..b8673522 --- /dev/null +++ b/src/icons/sensor-icon.ts @@ -0,0 +1,112 @@ +import { UNIT_C, UNIT_F } from "custom-card-helpers"; +import { HassEntity } from "home-assistant-js-websocket"; + +const FIXED_DEVICE_CLASS_ICONS = { + apparent_power: "mdi:flash", + aqi: "mdi:air-filter", + carbon_dioxide: "mdi:molecule-co2", + carbon_monoxide: "mdi:molecule-co", + current: "mdi:current-ac", + date: "mdi:calendar", + energy: "mdi:lightning-bolt", + frequency: "mdi:sine-wave", + gas: "mdi:gas-cylinder", + humidity: "mdi:water-percent", + illuminance: "mdi:brightness-5", + monetary: "mdi:cash", + nitrogen_dioxide: "mdi:molecule", + nitrogen_monoxide: "mdi:molecule", + nitrous_oxide: "mdi:molecule", + ozone: "mdi:molecule", + pm1: "mdi:molecule", + pm10: "mdi:molecule", + pm25: "mdi:molecule", + power: "mdi:flash", + power_factor: "mdi:angle-acute", + pressure: "mdi:gauge", + reactive_power: "mdi:flash", + signal_strength: "mdi:wifi", + sulphur_dioxide: "mdi:molecule", + temperature: "mdi:thermometer", + timestamp: "mdi:clock", + volatile_organic_compounds: "mdi:molecule", + voltage: "mdi:sine-wave", +}; + +const SENSOR_DEVICE_CLASS_BATTERY = "battery"; + +const BATTERY_ICONS = { + 10: "mdi:battery-10", + 20: "mdi:battery-20", + 30: "mdi:battery-30", + 40: "mdi:battery-40", + 50: "mdi:battery-50", + 60: "mdi:battery-60", + 70: "mdi:battery-70", + 80: "mdi:battery-80", + 90: "mdi:battery-90", + 100: "mdi:battery", +}; +const BATTERY_CHARGING_ICONS = { + 10: "mdi:battery-charging-10", + 20: "mdi:battery-charging-20", + 30: "mdi:battery-charging-30", + 40: "mdi:battery-charging-40", + 50: "mdi:battery-charging-50", + 60: "mdi:battery-charging-60", + 70: "mdi:battery-charging-70", + 80: "mdi:battery-charging-80", + 90: "mdi:battery-charging-90", + 100: "mdi:battery-charging", +}; + +const batteryStateIcon = (batteryEntity: HassEntity, batteryChargingEntity?: HassEntity) => { + const battery = batteryEntity.state; + const batteryCharging = batteryChargingEntity?.state === "on"; + + return batteryIcon(battery, batteryCharging); +}; + +export const batteryIcon = (batteryState: number | string, batteryCharging?: boolean) => { + const batteryValue = Number(batteryState); + if (isNaN(batteryValue)) { + if (batteryState === "off") { + return "mdi:battery"; + } + if (batteryState === "on") { + return "mdi:battery-alert"; + } + return "mdi:battery-unknown"; + } + + const batteryRound = Math.round(batteryValue / 10) * 10; + if (batteryCharging && batteryValue >= 10) { + return BATTERY_CHARGING_ICONS[batteryRound]; + } + if (batteryCharging) { + return "mdi:battery-charging-outline"; + } + if (batteryValue <= 5) { + return "mdi:battery-alert-variant-outline"; + } + return BATTERY_ICONS[batteryRound]; +}; + +export const sensorIcon = (entity?: HassEntity): string | undefined => { + const dclass = entity?.attributes.device_class; + + if (dclass && dclass in FIXED_DEVICE_CLASS_ICONS) { + return FIXED_DEVICE_CLASS_ICONS[dclass]; + } + + if (dclass === SENSOR_DEVICE_CLASS_BATTERY) { + return entity ? batteryStateIcon(entity) : "mdi:battery"; + } + + const unit = entity?.attributes.unit_of_measurement; + if (unit === UNIT_C || unit === UNIT_F) { + return "mdi:thermometer"; + } + + return undefined; +}; diff --git a/src/icons/state-icon.ts b/src/icons/state-icon.ts new file mode 100644 index 00000000..44d8e8da --- /dev/null +++ b/src/icons/state-icon.ts @@ -0,0 +1,18 @@ +import { computeDomain } from "custom-card-helpers"; +import { HassEntity } from "home-assistant-js-websocket"; +import { DEFAULT_DOMAIN_ICON, domainIcon } from "./domain-icon"; + +export function stateIcon(entity?: HassEntity | null): string { + if (!entity) { + return DEFAULT_DOMAIN_ICON; + } + if (entity.attributes.icon) { + return entity.attributes.icon; + } + + const domain = computeDomain(entity.entity_id); + + const state = entity.state; + + return domainIcon(domain, entity, state); +} diff --git a/src/icons/update.ts b/src/icons/update.ts new file mode 100644 index 00000000..4fa2d460 --- /dev/null +++ b/src/icons/update.ts @@ -0,0 +1,32 @@ +import type { HassEntity, HassEntityAttributeBase, HassEntityBase } from "home-assistant-js-websocket"; + +export const UPDATE_SUPPORT_INSTALL = 1; +export const UPDATE_SUPPORT_SPECIFIC_VERSION = 2; +export const UPDATE_SUPPORT_PROGRESS = 4; +export const UPDATE_SUPPORT_BACKUP = 8; +export const UPDATE_SUPPORT_RELEASE_NOTES = 16; + +interface UpdateEntityAttributes extends HassEntityAttributeBase { + auto_update: boolean | null; + installed_version: string | null; + in_progress: boolean | number; + latest_version: string | null; + release_summary: string | null; + release_url: string | null; + skipped_version: string | null; + title: string | null; +} + +export interface UpdateEntity extends HassEntityBase { + attributes: UpdateEntityAttributes; +} + +export const supportsFeature = (stateObj: HassEntity, feature: number): boolean => + ((stateObj.attributes.supported_features ?? 0) & feature) !== 0; + +export const updateUsesProgress = (entity: UpdateEntity): boolean => + supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) && + typeof entity.attributes.in_progress === "number"; + +export const updateIsInstalling = (entity: UpdateEntity): boolean => + updateUsesProgress(entity) || !!entity.attributes.in_progress;