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.umd.js CHANGED
@@ -1,8 +1,8 @@
1
1
  (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@formatjs/intl-utils')) :
3
- typeof define === 'function' && define.amd ? define(['exports', '@formatjs/intl-utils'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.customCardHelpers = {}, global.intlUtils));
5
- })(this, (function (exports, intlUtils) { 'use strict';
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.customCardHelpers = {}));
5
+ })(this, (function (exports) { 'use strict';
6
6
 
7
7
  /**
8
8
  * Apply a theme to an element by setting the CSS variables on it.
@@ -89,6 +89,13 @@
89
89
  return DOMAIN_ICONS[domain] || "mdi:bookmark";
90
90
  };
91
91
 
92
+ /**
93
+ * @deprecated Reads `friendly_name`, which Home Assistant is moving away from -
94
+ * it now always includes the device name, so this duplicates context the
95
+ * surrounding UI already shows. Prefer `hass.formatEntityName(stateObj, name)`
96
+ * on HA 2026.4 and later, which resolves the name from the entity's registry
97
+ * context and lets users choose which parts to include.
98
+ */
92
99
  const computeName = (stateObj) => stateObj.attributes.friendly_name || stateObj.entity_id;
93
100
 
94
101
  function computeRTL(hass) {
@@ -139,6 +146,30 @@
139
146
  TimeFormat["am_pm"] = "12";
140
147
  TimeFormat["twenty_four"] = "24";
141
148
  })(exports.TimeFormat || (exports.TimeFormat = {}));
149
+ exports.TimeZone = void 0;
150
+ (function (TimeZone) {
151
+ TimeZone["local"] = "local";
152
+ TimeZone["server"] = "server";
153
+ })(exports.TimeZone || (exports.TimeZone = {}));
154
+ exports.DateFormat = void 0;
155
+ (function (DateFormat) {
156
+ DateFormat["language"] = "language";
157
+ DateFormat["system"] = "system";
158
+ DateFormat["DMY"] = "DMY";
159
+ DateFormat["MDY"] = "MDY";
160
+ DateFormat["YMD"] = "YMD";
161
+ })(exports.DateFormat || (exports.DateFormat = {}));
162
+ exports.FirstWeekday = void 0;
163
+ (function (FirstWeekday) {
164
+ FirstWeekday["language"] = "language";
165
+ FirstWeekday["monday"] = "monday";
166
+ FirstWeekday["tuesday"] = "tuesday";
167
+ FirstWeekday["wednesday"] = "wednesday";
168
+ FirstWeekday["thursday"] = "thursday";
169
+ FirstWeekday["friday"] = "friday";
170
+ FirstWeekday["saturday"] = "saturday";
171
+ FirstWeekday["sunday"] = "sunday";
172
+ })(exports.FirstWeekday || (exports.FirstWeekday = {}));
142
173
 
143
174
  // REF: https://github.com/home-assistant/frontend/blob/dev/src/common/datetime/use_am_pm.ts
144
175
  /**
@@ -737,6 +768,74 @@
737
768
  return parts[0] * 3600 + parts[1] * 60 + parts[2];
738
769
  }
739
770
 
771
+ /*
772
+ * selectUnit() taken from @formatjs/intl-units 3.8.4.
773
+ *
774
+ */
775
+ const MS_PER_SECOND = 1e3;
776
+ const SECS_PER_MIN = 60;
777
+ const SECS_PER_HOUR = SECS_PER_MIN * 60;
778
+ const SECS_PER_DAY = SECS_PER_HOUR * 24;
779
+ const SECS_PER_WEEK = SECS_PER_DAY * 7;
780
+ function selectUnit(from, to = Date.now(), thresholds = {}) {
781
+ const resolvedThresholds = Object.assign(Object.assign({}, DEFAULT_THRESHOLDS), (thresholds || {}));
782
+ const secs = (+from - +to) / MS_PER_SECOND;
783
+ if (Math.abs(secs) < resolvedThresholds.second) {
784
+ return {
785
+ value: Math.round(secs),
786
+ unit: 'second',
787
+ };
788
+ }
789
+ const mins = secs / SECS_PER_MIN;
790
+ if (Math.abs(mins) < resolvedThresholds.minute) {
791
+ return {
792
+ value: Math.round(mins),
793
+ unit: 'minute',
794
+ };
795
+ }
796
+ const hours = secs / SECS_PER_HOUR;
797
+ if (Math.abs(hours) < resolvedThresholds.hour) {
798
+ return {
799
+ value: Math.round(hours),
800
+ unit: 'hour',
801
+ };
802
+ }
803
+ const days = secs / SECS_PER_DAY;
804
+ if (Math.abs(days) < resolvedThresholds.day) {
805
+ return {
806
+ value: Math.round(days),
807
+ unit: 'day',
808
+ };
809
+ }
810
+ const fromDate = new Date(from);
811
+ const toDate = new Date(to);
812
+ const years = fromDate.getFullYear() - toDate.getFullYear();
813
+ if (Math.round(Math.abs(years)) > 0) {
814
+ return {
815
+ value: Math.round(years),
816
+ unit: 'year',
817
+ };
818
+ }
819
+ const months = years * 12 + fromDate.getMonth() - toDate.getMonth();
820
+ if (Math.round(Math.abs(months)) > 0) {
821
+ return {
822
+ value: Math.round(months),
823
+ unit: 'month',
824
+ };
825
+ }
826
+ const weeks = secs / SECS_PER_WEEK;
827
+ return {
828
+ value: Math.round(weeks),
829
+ unit: 'week',
830
+ };
831
+ }
832
+ const DEFAULT_THRESHOLDS = {
833
+ second: 45, // seconds to minute
834
+ minute: 45, // minutes to hour
835
+ hour: 22, // hour to day
836
+ day: 5, // day to week
837
+ };
838
+
740
839
  //REF: https://github.com/home-assistant/frontend/blob/dev/src/common/datetime/relative_time.ts
741
840
  const formatRelTimeMem = (locale) => new Intl.RelativeTimeFormat(locale.language, { numeric: "auto" });
742
841
  /**
@@ -745,7 +844,7 @@
745
844
  * Example output: 5 minutes ago, in 3 days.
746
845
  */
747
846
  const relativeTime = (from, locale, to, includeTense = true) => {
748
- const diff = intlUtils.selectUnit(from, to);
847
+ const diff = selectUnit(from, to);
749
848
  if (includeTense) {
750
849
  return formatRelTimeMem(locale).format(diff.value, diff.unit);
751
850
  }
@@ -756,6 +855,24 @@
756
855
  }).format(Math.abs(diff.value));
757
856
  };
758
857
 
858
+ //REF: https://github.com/home-assistant/frontend/blob/dev/src/common/datetime/seconds_to_duration.ts
859
+ const leftPad = (num) => (num < 10 ? `0${num}` : num);
860
+ function secondsToDuration(d) {
861
+ const h = Math.floor(d / 3600);
862
+ const m = Math.floor((d % 3600) / 60);
863
+ const s = Math.floor((d % 3600) % 60);
864
+ if (h > 0) {
865
+ return `${h}:${leftPad(m)}:${leftPad(s)}`;
866
+ }
867
+ if (m > 0) {
868
+ return `${m}:${leftPad(s)}`;
869
+ }
870
+ if (s > 0) {
871
+ return "" + s;
872
+ }
873
+ return null;
874
+ }
875
+
759
876
  function timerTimeRemaining(stateObj) {
760
877
  let timeRemaining = durationToSeconds(stateObj.attributes.remaining);
761
878
  if (stateObj.state === "active") {
@@ -1043,18 +1160,15 @@
1043
1160
  };
1044
1161
 
1045
1162
  const getLovelace = () => {
1046
- let root = document.querySelector('home-assistant');
1047
- root = root && root.shadowRoot;
1048
- root = root && root.querySelector('home-assistant-main');
1049
- root = root && root.shadowRoot;
1050
- root = root && root.querySelector('app-drawer-layout partial-panel-resolver');
1051
- root = root && root.shadowRoot || root;
1052
- root = root && root.querySelector('ha-panel-lovelace');
1053
- root = root && root.shadowRoot;
1054
- root = root && root.querySelector('hui-root');
1055
- if (root) {
1056
- const ll = root.lovelace;
1057
- ll.current_view = root.___curView;
1163
+ var _a, _b, _c, _d, _e, _f;
1164
+ const root = (_c = (_b = (_a = document
1165
+ .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;
1166
+ const resolver = (root === null || root === void 0 ? void 0 : root.querySelector("ha-drawer partial-panel-resolver")) ||
1167
+ (root === null || root === void 0 ? void 0 : root.querySelector("app-drawer-layout partial-panel-resolver"));
1168
+ 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");
1169
+ if (huiRoot) {
1170
+ const ll = huiRoot.lovelace;
1171
+ ll.current_view = huiRoot.___curView;
1058
1172
  return ll;
1059
1173
  }
1060
1174
  return null;
@@ -1092,6 +1206,8 @@
1092
1206
  }
1093
1207
  return hass.callService(serviceDomain, service, { entity_id: entityId });
1094
1208
  };
1209
+ const turnOnEntity = (hass, entityId) => turnOnOffEntity(hass, entityId, true);
1210
+ const turnOffEntity = (hass, entityId) => turnOnOffEntity(hass, entityId, false);
1095
1211
 
1096
1212
  const toggleEntity = (hass, entityId) => {
1097
1213
  const turnOn = STATES_OFF.includes(hass.states[entityId].state);
@@ -1137,13 +1253,21 @@
1137
1253
  forwardHaptic("success");
1138
1254
  }
1139
1255
  break;
1256
+ case "turn_on":
1257
+ turnOnOffEntity(hass, actionConfig.entity || config.entity, true);
1258
+ forwardHaptic("success");
1259
+ break;
1260
+ case "turn_off":
1261
+ turnOnOffEntity(hass, actionConfig.entity || config.entity, false);
1262
+ forwardHaptic("success");
1263
+ break;
1140
1264
  case "call-service": {
1141
1265
  if (!actionConfig.service) {
1142
1266
  forwardHaptic("failure");
1143
1267
  return;
1144
1268
  }
1145
1269
  const [domain, service] = actionConfig.service.split(".", 2);
1146
- hass.callService(domain, service, actionConfig.service_data, actionConfig.target);
1270
+ hass.callService(domain, service, actionConfig.data, actionConfig.target);
1147
1271
  forwardHaptic("success");
1148
1272
  break;
1149
1273
  }
@@ -1228,7 +1352,7 @@
1228
1352
  return;
1229
1353
  }
1230
1354
  const [domain, service] = actionConfig.service.split(".", 2);
1231
- const serviceData = Object.assign({}, actionConfig.service_data);
1355
+ const serviceData = Object.assign({}, actionConfig.data);
1232
1356
  if (serviceData.entity_id === "entity") {
1233
1357
  serviceData.entity_id = config.entity;
1234
1358
  }
@@ -1273,6 +1397,46 @@
1273
1397
  return config !== undefined && config.action !== "none";
1274
1398
  }
1275
1399
 
1400
+ /**
1401
+ * Scroll to a specific y coordinate.
1402
+ *
1403
+ * Copied from paper-scroll-header-panel.
1404
+ *
1405
+ * @method scroll
1406
+ * @param {number} top The coordinate to scroll to, along the y-axis.
1407
+ * @param {boolean} smooth true if the scroll position should be smoothly adjusted.
1408
+ */
1409
+ function scrollToTarget(element, target) {
1410
+ // the scroll event will trigger _updateScrollState directly,
1411
+ // However, _updateScrollState relies on the previous `scrollTop` to update the states.
1412
+ // Calling _updateScrollState will ensure that the states are synced correctly.
1413
+ const top = 0;
1414
+ const scroller = target;
1415
+ const easingFn = function easeOutQuad(t, b, c, d) {
1416
+ /* eslint-disable no-param-reassign, space-infix-ops, no-mixed-operators */
1417
+ t /= d;
1418
+ return -c * t * (t - 2) + b;
1419
+ /* eslint-enable no-param-reassign, space-infix-ops, no-mixed-operators */
1420
+ };
1421
+ const animationId = Math.random();
1422
+ const duration = 200;
1423
+ const startTime = Date.now();
1424
+ const currentScrollTop = scroller.scrollTop;
1425
+ const deltaScrollTop = top - currentScrollTop;
1426
+ element._currentAnimationId = animationId;
1427
+ (function updateFrame() {
1428
+ const now = Date.now();
1429
+ const elapsedTime = now - startTime;
1430
+ if (elapsedTime > duration) {
1431
+ scroller.scrollTop = top;
1432
+ }
1433
+ else if (element._currentAnimationId === animationId) {
1434
+ scroller.scrollTop = easingFn(elapsedTime, currentScrollTop, deltaScrollTop, duration);
1435
+ requestAnimationFrame(updateFrame.bind(element));
1436
+ }
1437
+ }.call(element));
1438
+ }
1439
+
1276
1440
  /** Return an icon representing a binary sensor state. */
1277
1441
  const binarySensorIcon = (state, stateObj) => {
1278
1442
  const is_off = state === "off";
@@ -1335,17 +1499,86 @@
1335
1499
  const open = state.state !== "closed";
1336
1500
  switch (state.attributes.device_class) {
1337
1501
  case "garage":
1338
- return open ? "mdi:garage-open" : "mdi:garage";
1502
+ switch (state.state) {
1503
+ case "opening":
1504
+ return "mdi:arrow-up-box";
1505
+ case "closing":
1506
+ return "mdi:arrow-down-box";
1507
+ case "closed":
1508
+ return "mdi:garage";
1509
+ default:
1510
+ return "mdi:garage-open";
1511
+ }
1512
+ case "gate":
1513
+ switch (state.state) {
1514
+ case "opening":
1515
+ case "closing":
1516
+ return "mdi:gate-arrow-right";
1517
+ case "closed":
1518
+ return "mdi:gate";
1519
+ default:
1520
+ return "mdi:gate-open";
1521
+ }
1339
1522
  case "door":
1340
1523
  return open ? "mdi:door-open" : "mdi:door-closed";
1524
+ case "damper":
1525
+ return open ? "mdi:circle" : "mdi:circle-slice-8";
1341
1526
  case "shutter":
1342
- return open ? "mdi:window-shutter-open" : "mdi:window-shutter";
1527
+ switch (state.state) {
1528
+ case "opening":
1529
+ return "mdi:arrow-up-box";
1530
+ case "closing":
1531
+ return "mdi:arrow-down-box";
1532
+ case "closed":
1533
+ return "mdi:window-shutter";
1534
+ default:
1535
+ return "mdi:window-shutter-open";
1536
+ }
1537
+ case "curtain":
1538
+ switch (state.state) {
1539
+ case "opening":
1540
+ return "mdi:arrow-split-vertical";
1541
+ case "closing":
1542
+ return "mdi:arrow-collapse-horizontal";
1543
+ case "closed":
1544
+ return "mdi:curtains-closed";
1545
+ default:
1546
+ return "mdi:curtains";
1547
+ }
1343
1548
  case "blind":
1344
- return open ? "mdi:blinds-open" : "mdi:blinds";
1549
+ case "shade":
1550
+ switch (state.state) {
1551
+ case "opening":
1552
+ return "mdi:arrow-up-box";
1553
+ case "closing":
1554
+ return "mdi:arrow-down-box";
1555
+ case "closed":
1556
+ return "mdi:blinds";
1557
+ default:
1558
+ return "mdi:blinds-open";
1559
+ }
1345
1560
  case "window":
1346
- return open ? "mdi:window-open" : "mdi:window-closed";
1561
+ switch (state.state) {
1562
+ case "opening":
1563
+ return "mdi:arrow-up-box";
1564
+ case "closing":
1565
+ return "mdi:arrow-down-box";
1566
+ case "closed":
1567
+ return "mdi:window-closed";
1568
+ default:
1569
+ return "mdi:window-open";
1570
+ }
1347
1571
  default:
1348
- return domainIcon("cover", state.state);
1572
+ switch (state.state) {
1573
+ case "opening":
1574
+ return "mdi:arrow-up-box";
1575
+ case "closing":
1576
+ return "mdi:arrow-down-box";
1577
+ case "closed":
1578
+ return "mdi:window-closed";
1579
+ default:
1580
+ return "mdi:window-open";
1581
+ }
1349
1582
  }
1350
1583
  };
1351
1584
 
@@ -1406,7 +1639,7 @@
1406
1639
  };
1407
1640
 
1408
1641
  const domainIcons = {
1409
- binary_sensor: binarySensorIcon,
1642
+ binary_sensor: (stateObj) => binarySensorIcon(stateObj.state, stateObj),
1410
1643
  cover: coverIcon,
1411
1644
  sensor: sensorIcon,
1412
1645
  input_datetime: inputDateTimeIcon,
@@ -1482,6 +1715,7 @@
1482
1715
  exports.debounce = debounce;
1483
1716
  exports.deepEqual = deepEqual;
1484
1717
  exports.domainIcon = domainIcon;
1718
+ exports.durationToSeconds = durationToSeconds;
1485
1719
  exports.evaluateFilter = evaluateFilter;
1486
1720
  exports.fireEvent = fireEvent;
1487
1721
  exports.fixedIcons = fixedIcons;
@@ -1513,9 +1747,13 @@
1513
1747
  exports.numberFormatToLocale = numberFormatToLocale;
1514
1748
  exports.relativeTime = relativeTime;
1515
1749
  exports.round = round;
1750
+ exports.scrollToTarget = scrollToTarget;
1751
+ exports.secondsToDuration = secondsToDuration;
1516
1752
  exports.stateIcon = stateIcon;
1517
1753
  exports.timerTimeRemaining = timerTimeRemaining;
1518
1754
  exports.toggleEntity = toggleEntity;
1755
+ exports.turnOffEntity = turnOffEntity;
1756
+ exports.turnOnEntity = turnOnEntity;
1519
1757
  exports.turnOnOffEntities = turnOnOffEntities;
1520
1758
  exports.turnOnOffEntity = turnOnOffEntity;
1521
1759