custom-card-helpers 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.github/dependabot.yml +13 -0
  2. package/.github/workflows/claude-code-review.yml +44 -0
  3. package/.github/workflows/claude.yml +50 -0
  4. package/.github/workflows/release.yml +4 -0
  5. package/.github/workflows/test.yml +1 -0
  6. package/dist/compute-name.d.ts +7 -0
  7. package/dist/compute-state-display.d.ts +1 -2
  8. package/dist/datetime/duration_to_seconds.d.ts +1 -1
  9. package/dist/datetime/seconds_to_duration.d.ts +1 -1
  10. package/dist/datetime/select_unit.d.ts +8 -0
  11. package/dist/get-lovelace.d.ts +5 -1
  12. package/dist/index.js +259 -23
  13. package/dist/index.js.map +1 -1
  14. package/dist/index.m.js +254 -23
  15. package/dist/index.m.js.map +1 -1
  16. package/dist/index.umd.js +263 -25
  17. package/dist/index.umd.js.map +1 -1
  18. package/dist/scroll-to-target.d.ts +1 -1
  19. package/dist/translations/localize.d.ts +1 -2
  20. package/dist/turn-on-off-entity.d.ts +2 -0
  21. package/dist/types.d.ts +156 -4
  22. package/package.json +7 -8
  23. package/src/compute-name.ts +7 -0
  24. package/src/compute-state-display.ts +2 -3
  25. package/src/cover_icon.ts +74 -6
  26. package/src/datetime/duration_to_seconds.ts +1 -1
  27. package/src/datetime/relative_time.ts +1 -2
  28. package/src/datetime/seconds_to_duration.ts +1 -1
  29. package/src/datetime/select_unit.ts +94 -0
  30. package/src/datetime/timer_time_remaining.ts +1 -1
  31. package/src/get-lovelace.ts +22 -16
  32. package/src/handle-action.ts +19 -2
  33. package/src/handle-click.ts +1 -1
  34. package/src/scroll-to-target.ts +1 -1
  35. package/src/state_icon.ts +1 -1
  36. package/src/translations/localize.ts +1 -3
  37. package/src/turn-on-off-entity.ts +10 -0
  38. package/src/types.ts +203 -3
  39. package/test/datetime.test.ts +2 -2
  40. package/test/icons.test.ts +51 -0
  41. package/test/service-actions.test.ts +19 -1
package/dist/index.m.js CHANGED
@@ -1,5 +1,3 @@
1
- import { selectUnit } from '@formatjs/intl-utils';
2
-
3
1
  /**
4
2
  * Apply a theme to an element by setting the CSS variables on it.
5
3
  *
@@ -85,6 +83,13 @@ const computeIcon = (stateObj, icon) => {
85
83
  return DOMAIN_ICONS[domain] || "mdi:bookmark";
86
84
  };
87
85
 
86
+ /**
87
+ * @deprecated Reads `friendly_name`, which Home Assistant is moving away from -
88
+ * it now always includes the device name, so this duplicates context the
89
+ * surrounding UI already shows. Prefer `hass.formatEntityName(stateObj, name)`
90
+ * on HA 2026.4 and later, which resolves the name from the entity's registry
91
+ * context and lets users choose which parts to include.
92
+ */
88
93
  const computeName = (stateObj) => stateObj.attributes.friendly_name || stateObj.entity_id;
89
94
 
90
95
  function computeRTL(hass) {
@@ -135,6 +140,30 @@ var TimeFormat;
135
140
  TimeFormat["am_pm"] = "12";
136
141
  TimeFormat["twenty_four"] = "24";
137
142
  })(TimeFormat || (TimeFormat = {}));
143
+ var TimeZone;
144
+ (function (TimeZone) {
145
+ TimeZone["local"] = "local";
146
+ TimeZone["server"] = "server";
147
+ })(TimeZone || (TimeZone = {}));
148
+ var DateFormat;
149
+ (function (DateFormat) {
150
+ DateFormat["language"] = "language";
151
+ DateFormat["system"] = "system";
152
+ DateFormat["DMY"] = "DMY";
153
+ DateFormat["MDY"] = "MDY";
154
+ DateFormat["YMD"] = "YMD";
155
+ })(DateFormat || (DateFormat = {}));
156
+ var FirstWeekday;
157
+ (function (FirstWeekday) {
158
+ FirstWeekday["language"] = "language";
159
+ FirstWeekday["monday"] = "monday";
160
+ FirstWeekday["tuesday"] = "tuesday";
161
+ FirstWeekday["wednesday"] = "wednesday";
162
+ FirstWeekday["thursday"] = "thursday";
163
+ FirstWeekday["friday"] = "friday";
164
+ FirstWeekday["saturday"] = "saturday";
165
+ FirstWeekday["sunday"] = "sunday";
166
+ })(FirstWeekday || (FirstWeekday = {}));
138
167
 
139
168
  // REF: https://github.com/home-assistant/frontend/blob/dev/src/common/datetime/use_am_pm.ts
140
169
  /**
@@ -733,6 +762,74 @@ function durationToSeconds(duration) {
733
762
  return parts[0] * 3600 + parts[1] * 60 + parts[2];
734
763
  }
735
764
 
765
+ /*
766
+ * selectUnit() taken from @formatjs/intl-units 3.8.4.
767
+ *
768
+ */
769
+ const MS_PER_SECOND = 1e3;
770
+ const SECS_PER_MIN = 60;
771
+ const SECS_PER_HOUR = SECS_PER_MIN * 60;
772
+ const SECS_PER_DAY = SECS_PER_HOUR * 24;
773
+ const SECS_PER_WEEK = SECS_PER_DAY * 7;
774
+ function selectUnit(from, to = Date.now(), thresholds = {}) {
775
+ const resolvedThresholds = Object.assign(Object.assign({}, DEFAULT_THRESHOLDS), (thresholds || {}));
776
+ const secs = (+from - +to) / MS_PER_SECOND;
777
+ if (Math.abs(secs) < resolvedThresholds.second) {
778
+ return {
779
+ value: Math.round(secs),
780
+ unit: 'second',
781
+ };
782
+ }
783
+ const mins = secs / SECS_PER_MIN;
784
+ if (Math.abs(mins) < resolvedThresholds.minute) {
785
+ return {
786
+ value: Math.round(mins),
787
+ unit: 'minute',
788
+ };
789
+ }
790
+ const hours = secs / SECS_PER_HOUR;
791
+ if (Math.abs(hours) < resolvedThresholds.hour) {
792
+ return {
793
+ value: Math.round(hours),
794
+ unit: 'hour',
795
+ };
796
+ }
797
+ const days = secs / SECS_PER_DAY;
798
+ if (Math.abs(days) < resolvedThresholds.day) {
799
+ return {
800
+ value: Math.round(days),
801
+ unit: 'day',
802
+ };
803
+ }
804
+ const fromDate = new Date(from);
805
+ const toDate = new Date(to);
806
+ const years = fromDate.getFullYear() - toDate.getFullYear();
807
+ if (Math.round(Math.abs(years)) > 0) {
808
+ return {
809
+ value: Math.round(years),
810
+ unit: 'year',
811
+ };
812
+ }
813
+ const months = years * 12 + fromDate.getMonth() - toDate.getMonth();
814
+ if (Math.round(Math.abs(months)) > 0) {
815
+ return {
816
+ value: Math.round(months),
817
+ unit: 'month',
818
+ };
819
+ }
820
+ const weeks = secs / SECS_PER_WEEK;
821
+ return {
822
+ value: Math.round(weeks),
823
+ unit: 'week',
824
+ };
825
+ }
826
+ const DEFAULT_THRESHOLDS = {
827
+ second: 45, // seconds to minute
828
+ minute: 45, // minutes to hour
829
+ hour: 22, // hour to day
830
+ day: 5, // day to week
831
+ };
832
+
736
833
  //REF: https://github.com/home-assistant/frontend/blob/dev/src/common/datetime/relative_time.ts
737
834
  const formatRelTimeMem = (locale) => new Intl.RelativeTimeFormat(locale.language, { numeric: "auto" });
738
835
  /**
@@ -752,6 +849,24 @@ const relativeTime = (from, locale, to, includeTense = true) => {
752
849
  }).format(Math.abs(diff.value));
753
850
  };
754
851
 
852
+ //REF: https://github.com/home-assistant/frontend/blob/dev/src/common/datetime/seconds_to_duration.ts
853
+ const leftPad = (num) => (num < 10 ? `0${num}` : num);
854
+ function secondsToDuration(d) {
855
+ const h = Math.floor(d / 3600);
856
+ const m = Math.floor((d % 3600) / 60);
857
+ const s = Math.floor((d % 3600) % 60);
858
+ if (h > 0) {
859
+ return `${h}:${leftPad(m)}:${leftPad(s)}`;
860
+ }
861
+ if (m > 0) {
862
+ return `${m}:${leftPad(s)}`;
863
+ }
864
+ if (s > 0) {
865
+ return "" + s;
866
+ }
867
+ return null;
868
+ }
869
+
755
870
  function timerTimeRemaining(stateObj) {
756
871
  let timeRemaining = durationToSeconds(stateObj.attributes.remaining);
757
872
  if (stateObj.state === "active") {
@@ -1039,18 +1154,15 @@ const formatTimestamp = (timestamp) => {
1039
1154
  };
1040
1155
 
1041
1156
  const getLovelace = () => {
1042
- let root = document.querySelector('home-assistant');
1043
- root = root && root.shadowRoot;
1044
- root = root && root.querySelector('home-assistant-main');
1045
- root = root && root.shadowRoot;
1046
- root = root && root.querySelector('app-drawer-layout partial-panel-resolver');
1047
- root = root && root.shadowRoot || root;
1048
- root = root && root.querySelector('ha-panel-lovelace');
1049
- root = root && root.shadowRoot;
1050
- root = root && root.querySelector('hui-root');
1051
- if (root) {
1052
- const ll = root.lovelace;
1053
- ll.current_view = root.___curView;
1157
+ var _a, _b, _c, _d, _e, _f;
1158
+ const root = (_c = (_b = (_a = document
1159
+ .querySelector("home-assistant")) === null || _a === void 0 ? void 0 : _a.shadowRoot) === null || _b === void 0 ? void 0 : _b.querySelector("home-assistant-main")) === null || _c === void 0 ? void 0 : _c.shadowRoot;
1160
+ const resolver = (root === null || root === void 0 ? void 0 : root.querySelector("ha-drawer partial-panel-resolver")) ||
1161
+ (root === null || root === void 0 ? void 0 : root.querySelector("app-drawer-layout partial-panel-resolver"));
1162
+ const huiRoot = (_f = (_e = (_d = ((resolver === null || resolver === void 0 ? void 0 : resolver.shadowRoot) || resolver)) === null || _d === void 0 ? void 0 : _d.querySelector("ha-panel-lovelace")) === null || _e === void 0 ? void 0 : _e.shadowRoot) === null || _f === void 0 ? void 0 : _f.querySelector("hui-root");
1163
+ if (huiRoot) {
1164
+ const ll = huiRoot.lovelace;
1165
+ ll.current_view = huiRoot.___curView;
1054
1166
  return ll;
1055
1167
  }
1056
1168
  return null;
@@ -1088,6 +1200,8 @@ const turnOnOffEntity = (hass, entityId, turnOn = true) => {
1088
1200
  }
1089
1201
  return hass.callService(serviceDomain, service, { entity_id: entityId });
1090
1202
  };
1203
+ const turnOnEntity = (hass, entityId) => turnOnOffEntity(hass, entityId, true);
1204
+ const turnOffEntity = (hass, entityId) => turnOnOffEntity(hass, entityId, false);
1091
1205
 
1092
1206
  const toggleEntity = (hass, entityId) => {
1093
1207
  const turnOn = STATES_OFF.includes(hass.states[entityId].state);
@@ -1133,13 +1247,21 @@ const handleActionConfig = (node, hass, config, actionConfig) => {
1133
1247
  forwardHaptic("success");
1134
1248
  }
1135
1249
  break;
1250
+ case "turn_on":
1251
+ turnOnOffEntity(hass, actionConfig.entity || config.entity, true);
1252
+ forwardHaptic("success");
1253
+ break;
1254
+ case "turn_off":
1255
+ turnOnOffEntity(hass, actionConfig.entity || config.entity, false);
1256
+ forwardHaptic("success");
1257
+ break;
1136
1258
  case "call-service": {
1137
1259
  if (!actionConfig.service) {
1138
1260
  forwardHaptic("failure");
1139
1261
  return;
1140
1262
  }
1141
1263
  const [domain, service] = actionConfig.service.split(".", 2);
1142
- hass.callService(domain, service, actionConfig.service_data, actionConfig.target);
1264
+ hass.callService(domain, service, actionConfig.data, actionConfig.target);
1143
1265
  forwardHaptic("success");
1144
1266
  break;
1145
1267
  }
@@ -1224,7 +1346,7 @@ const handleClick = (node, hass, config, hold, dblClick) => {
1224
1346
  return;
1225
1347
  }
1226
1348
  const [domain, service] = actionConfig.service.split(".", 2);
1227
- const serviceData = Object.assign({}, actionConfig.service_data);
1349
+ const serviceData = Object.assign({}, actionConfig.data);
1228
1350
  if (serviceData.entity_id === "entity") {
1229
1351
  serviceData.entity_id = config.entity;
1230
1352
  }
@@ -1269,6 +1391,46 @@ function hasDoubleClick(config) {
1269
1391
  return config !== undefined && config.action !== "none";
1270
1392
  }
1271
1393
 
1394
+ /**
1395
+ * Scroll to a specific y coordinate.
1396
+ *
1397
+ * Copied from paper-scroll-header-panel.
1398
+ *
1399
+ * @method scroll
1400
+ * @param {number} top The coordinate to scroll to, along the y-axis.
1401
+ * @param {boolean} smooth true if the scroll position should be smoothly adjusted.
1402
+ */
1403
+ function scrollToTarget(element, target) {
1404
+ // the scroll event will trigger _updateScrollState directly,
1405
+ // However, _updateScrollState relies on the previous `scrollTop` to update the states.
1406
+ // Calling _updateScrollState will ensure that the states are synced correctly.
1407
+ const top = 0;
1408
+ const scroller = target;
1409
+ const easingFn = function easeOutQuad(t, b, c, d) {
1410
+ /* eslint-disable no-param-reassign, space-infix-ops, no-mixed-operators */
1411
+ t /= d;
1412
+ return -c * t * (t - 2) + b;
1413
+ /* eslint-enable no-param-reassign, space-infix-ops, no-mixed-operators */
1414
+ };
1415
+ const animationId = Math.random();
1416
+ const duration = 200;
1417
+ const startTime = Date.now();
1418
+ const currentScrollTop = scroller.scrollTop;
1419
+ const deltaScrollTop = top - currentScrollTop;
1420
+ element._currentAnimationId = animationId;
1421
+ (function updateFrame() {
1422
+ const now = Date.now();
1423
+ const elapsedTime = now - startTime;
1424
+ if (elapsedTime > duration) {
1425
+ scroller.scrollTop = top;
1426
+ }
1427
+ else if (element._currentAnimationId === animationId) {
1428
+ scroller.scrollTop = easingFn(elapsedTime, currentScrollTop, deltaScrollTop, duration);
1429
+ requestAnimationFrame(updateFrame.bind(element));
1430
+ }
1431
+ }.call(element));
1432
+ }
1433
+
1272
1434
  /** Return an icon representing a binary sensor state. */
1273
1435
  const binarySensorIcon = (state, stateObj) => {
1274
1436
  const is_off = state === "off";
@@ -1331,17 +1493,86 @@ const coverIcon = (state) => {
1331
1493
  const open = state.state !== "closed";
1332
1494
  switch (state.attributes.device_class) {
1333
1495
  case "garage":
1334
- return open ? "mdi:garage-open" : "mdi:garage";
1496
+ switch (state.state) {
1497
+ case "opening":
1498
+ return "mdi:arrow-up-box";
1499
+ case "closing":
1500
+ return "mdi:arrow-down-box";
1501
+ case "closed":
1502
+ return "mdi:garage";
1503
+ default:
1504
+ return "mdi:garage-open";
1505
+ }
1506
+ case "gate":
1507
+ switch (state.state) {
1508
+ case "opening":
1509
+ case "closing":
1510
+ return "mdi:gate-arrow-right";
1511
+ case "closed":
1512
+ return "mdi:gate";
1513
+ default:
1514
+ return "mdi:gate-open";
1515
+ }
1335
1516
  case "door":
1336
1517
  return open ? "mdi:door-open" : "mdi:door-closed";
1518
+ case "damper":
1519
+ return open ? "mdi:circle" : "mdi:circle-slice-8";
1337
1520
  case "shutter":
1338
- return open ? "mdi:window-shutter-open" : "mdi:window-shutter";
1521
+ switch (state.state) {
1522
+ case "opening":
1523
+ return "mdi:arrow-up-box";
1524
+ case "closing":
1525
+ return "mdi:arrow-down-box";
1526
+ case "closed":
1527
+ return "mdi:window-shutter";
1528
+ default:
1529
+ return "mdi:window-shutter-open";
1530
+ }
1531
+ case "curtain":
1532
+ switch (state.state) {
1533
+ case "opening":
1534
+ return "mdi:arrow-split-vertical";
1535
+ case "closing":
1536
+ return "mdi:arrow-collapse-horizontal";
1537
+ case "closed":
1538
+ return "mdi:curtains-closed";
1539
+ default:
1540
+ return "mdi:curtains";
1541
+ }
1339
1542
  case "blind":
1340
- return open ? "mdi:blinds-open" : "mdi:blinds";
1543
+ case "shade":
1544
+ switch (state.state) {
1545
+ case "opening":
1546
+ return "mdi:arrow-up-box";
1547
+ case "closing":
1548
+ return "mdi:arrow-down-box";
1549
+ case "closed":
1550
+ return "mdi:blinds";
1551
+ default:
1552
+ return "mdi:blinds-open";
1553
+ }
1341
1554
  case "window":
1342
- return open ? "mdi:window-open" : "mdi:window-closed";
1555
+ switch (state.state) {
1556
+ case "opening":
1557
+ return "mdi:arrow-up-box";
1558
+ case "closing":
1559
+ return "mdi:arrow-down-box";
1560
+ case "closed":
1561
+ return "mdi:window-closed";
1562
+ default:
1563
+ return "mdi:window-open";
1564
+ }
1343
1565
  default:
1344
- return domainIcon("cover", state.state);
1566
+ switch (state.state) {
1567
+ case "opening":
1568
+ return "mdi:arrow-up-box";
1569
+ case "closing":
1570
+ return "mdi:arrow-down-box";
1571
+ case "closed":
1572
+ return "mdi:window-closed";
1573
+ default:
1574
+ return "mdi:window-open";
1575
+ }
1345
1576
  }
1346
1577
  };
1347
1578
 
@@ -1402,7 +1633,7 @@ const inputDateTimeIcon = (state) => {
1402
1633
  };
1403
1634
 
1404
1635
  const domainIcons = {
1405
- binary_sensor: binarySensorIcon,
1636
+ binary_sensor: (stateObj) => binarySensorIcon(stateObj.state, stateObj),
1406
1637
  cover: coverIcon,
1407
1638
  sensor: sensorIcon,
1408
1639
  input_datetime: inputDateTimeIcon,
@@ -1452,5 +1683,5 @@ const turnOnOffEntities = (hass, entityIds, turnOn = true) => {
1452
1683
  });
1453
1684
  };
1454
1685
 
1455
- export { DEFAULT_DOMAIN_ICON, DEFAULT_PANEL, DEFAULT_VIEW_ENTITY_ID, DOMAINS_HIDE_MORE_INFO, DOMAINS_MORE_INFO_NO_HISTORY, DOMAINS_TOGGLE, DOMAINS_WITH_CARD, DOMAINS_WITH_MORE_INFO, NumberFormat, STATES_OFF, TimeFormat, UNIT_C, UNIT_F, applyThemesOnElement, computeCardSize, computeDomain, computeEntity, computeIcon, computeName, computeRTL, computeRTLDirection, computeState, computeStateDisplay, computeStateDomain, createThing, debounce, deepEqual, domainIcon, evaluateFilter, fireEvent, fixedIcons, formatDate, formatDateMonth, formatDateMonthYear, formatDateNumeric, formatDateShort, formatDateTime, formatDateTimeNumeric, formatDateTimeWithSeconds, formatDateWeekday, formatDateYear, formatNumber, formatTime, formatTimeWeekday, formatTimeWithSeconds, formatTimestamp, forwardHaptic, getLovelace, handleAction, handleActionConfig, handleClick, hasAction, hasConfigOrEntityChanged, hasDoubleClick, isNumericState, navigate, numberFormatToLocale, relativeTime, round, stateIcon, timerTimeRemaining, toggleEntity, turnOnOffEntities, turnOnOffEntity };
1686
+ export { DEFAULT_DOMAIN_ICON, DEFAULT_PANEL, DEFAULT_VIEW_ENTITY_ID, DOMAINS_HIDE_MORE_INFO, DOMAINS_MORE_INFO_NO_HISTORY, DOMAINS_TOGGLE, DOMAINS_WITH_CARD, DOMAINS_WITH_MORE_INFO, DateFormat, FirstWeekday, NumberFormat, STATES_OFF, TimeFormat, TimeZone, UNIT_C, UNIT_F, applyThemesOnElement, computeCardSize, computeDomain, computeEntity, computeIcon, computeName, computeRTL, computeRTLDirection, computeState, computeStateDisplay, computeStateDomain, createThing, debounce, deepEqual, domainIcon, durationToSeconds, evaluateFilter, fireEvent, fixedIcons, formatDate, formatDateMonth, formatDateMonthYear, formatDateNumeric, formatDateShort, formatDateTime, formatDateTimeNumeric, formatDateTimeWithSeconds, formatDateWeekday, formatDateYear, formatNumber, formatTime, formatTimeWeekday, formatTimeWithSeconds, formatTimestamp, forwardHaptic, getLovelace, handleAction, handleActionConfig, handleClick, hasAction, hasConfigOrEntityChanged, hasDoubleClick, isNumericState, navigate, numberFormatToLocale, relativeTime, round, scrollToTarget, secondsToDuration, stateIcon, timerTimeRemaining, toggleEntity, turnOffEntity, turnOnEntity, turnOnOffEntities, turnOnOffEntity };
1456
1687
  //# sourceMappingURL=index.m.js.map