/** * @fileoverview * * This file defines a custom element `EntityProgressCard` for displaying * progress or status information about an entity in Home Assistant. * The card displays visual elements like icons, progress bars, and other * dynamic content based on the state of the entity and user configurations. * * Key Features: * - Dynamic content update (e.g., progress bar, icons) based on entity state. * - Support for theme and layout customization. * - Error handling for missing or invalid entities. * - Configuration options for various card elements. * * More informations here: * https://github.com/francois-le-ko4la/lovelace-entity-progress-card/ * * @author ko4la * @version 1.5.3-dev * */ /** -------------------------------------------------------------------------- * PARAMETERS */ // prettier-ignore-start const VERSION = '1.5.3-dev'; const META = { documentation: 'https://github.com/francois-le-ko4la/lovelace-entity-progress-card/', types: { card: { typeName: 'entity-progress-card', name: 'Entity Progress Card', description: 'A cool custom card to show current entity status with a progress bar.', editor: 'entity-progress-card-editor', }, template: { typeName: 'entity-progress-card-template', name: 'Entity Progress Card (Template)', description: 'A cool custom card to show current entity status with a progress bar.', editor: 'entity-progress-card-template-editor', }, badge: { typeName: 'entity-progress-badge', name: 'Entity Progress Badge', description: 'A cool custom badge to show current entity status with a progress bar.', editor: 'entity-progress-badge-editor', }, badgeTemplate: { typeName: 'entity-progress-badge-template', name: 'Entity Progress Badge (Template)', description: 'A cool custom badge to show current entity status with a progress bar.', editor: 'entity-progress-badge-template-editor', }, feature: { typeName: 'entity-progress-feature', name: 'Entity Progress Feature', description: 'A cool custom feature in tile to show current entity status with a progress bar.', }, } }; const CARD_CONTEXT = { dev: true, // editor: true, interactionHandler: true debug: { card: true, editor: true, interactionHandler: true, ressourceManager: true, hass: false }, }; // from: https://github.com/home-assistant/frontend/blob/master/src/resources/theme/color/color.globals.ts const HA_CONTEXT = { icons: { prefix: 'mdi:', help: 'mdi:help', helpCircle: 'mdi:help-circle', helpCircleOutline: 'mdi:help-circle-outline', chevronUpBox: 'mdi:chevron-up-box', chevronDownBox: 'mdi:chevron-down-box', equalBox: 'mdi:equal-box', focusHorizontal: 'mdi:focus-field-horizontal', focusVertical: 'mdi:focus-field-vertical', alert: 'mdi:alert', alertCircleOutline: 'mdi:alert-circle-outline', exclamationThick: 'mdi:exclamation-thick', play: 'mdi:play', pause: 'mdi:pause', gestureTapHold: 'mdi:gesture-tap-hold', washingMachine: 'mdi:washing-machine', update: 'mdi:update', lightbulb: 'mdi:lightbulb', lightbulbOutline: 'mdi:lightbulb-outline', thermometer: 'mdi:thermometer', waterPercent: 'mdi:water-percent', airFilter: 'mdi:air-filter', listBox: 'mdi:list-box', textShort: 'mdi:text-short', sizeSmall: 'mdi:size-s', sizeMedium: 'mdi:size-m', sizeLarge: 'mdi:size-l', sizeXLarge: 'mdi:size-xl', }, colors: { success: 'var(--success-color)', stateIcon: 'var(--state-icon-color)', red: 'var(--red-color)', orange: 'var(--orange-color)', deepOrange: 'var(--deep-orange-color)', yellow: 'var(--yellow-color)', amber: 'var(--amber-color)', accent: 'var(--accent-color)', deepPurple: 'var(--deep-purple-color)', indigo: 'var(--indigo-color)', blue: 'var(--blue-color)', lightBlue: 'var(--light-blue-color)', cyan: 'var(--cyan-color)', teal: 'var(--teal-color)', green: 'var(--green-color)', lightGreen: 'var(--light-green-color)', darkGrey: 'var(--dark-grey-color)', unavailable: 'var(--state-unavailable-color)', inactive: 'var(--state-inactive-color)', active: 'var(--state-active-color)', coverActive: 'var(--state-cover-active-color)', fanActive: 'var(--state-fan-active-color)', batteryLow: 'var(--state-sensor-battery-low-color)', batteryMedium: 'var(--state-sensor-battery-medium-color)', batteryHigh: 'var(--state-sensor-battery-high-color)', climateDry: 'var(--state-climate-dry-color)', climateCool: 'var(--state-climate-cool-color)', climateHeat: 'var(--state-climate-heat-color)', climateFanOnly: 'var(--state-climate-fan_only-color)', }, haColors: new Map( [ // texte 'primary-text', 'secondary-text', 'text-primary', 'text-light-primary', 'disabled-text', // interface 'dark-primary', 'darker-primary', 'light-primary', 'divider', 'outline', 'outline-hover', 'shadow', // material color 'primary', 'accent', 'red', 'pink', 'purple', 'deep-purple', 'indigo', 'blue', 'light-blue', 'cyan', 'teal', 'green', 'light-green', 'lime', 'yellow', 'amber', 'orange', 'deep-orange', 'brown', 'light-grey', 'grey', 'dark-grey', 'blue-grey', 'black', 'white', // HA 'success', 'warning', 'error', 'info', 'disabled', // State 'state-icon', 'state-active', 'state-inactive', 'state-unavailable', 'state-alarm_control_panel-armed_away', 'state-alarm_control_panel-armed_custom_bypass', 'state-alarm_control_panel-armed_home', 'state-alarm_control_panel-armed_night', 'state-alarm_control_panel-armed_vacation', 'state-alarm_control_panel-arming', 'state-alarm_control_panel-disarming', 'state-alarm_control_panel-pending', 'state-alarm_control_panel-triggered', 'state-alert-off', 'state-alert-on', 'state-binary_sensor-active', 'state-binary_sensor-battery-on', 'state-binary_sensor-carbon_monoxide-on', 'state-binary_sensor-gas-on', 'state-binary_sensor-heat-on', 'state-binary_sensor-lock-on', 'state-binary_sensor-moisture-on', 'state-binary_sensor-problem-on', 'state-binary_sensor-safety-on', 'state-binary_sensor-smoke-on', 'state-binary_sensor-sound-on', 'state-binary_sensor-tamper-on', 'state-climate-auto', 'state-climate-cool', 'state-climate-dry', 'state-climate-fan_only', 'state-climate-heat', 'state-climate-heat-cool', 'state-cover-active', 'state-device_tracker-active', 'state-device_tracker-home', 'state-fan-active', 'state-humidifier-on', 'state-lawn_mower-error', 'state-lawn_mower-mowing', 'state-light-active', 'state-lock-jammed', 'state-lock-locked', 'state-lock-locking', 'state-lock-unlocked', 'state-lock-unlocking', 'state-lock-open', 'state-lock-opening', 'state-media_player-active', 'state-person-active', 'state-person-home', 'state-plant-active', 'state-siren-active', 'state-sun-above_horizon', 'state-sun-below_horizon', 'state-switch-active', 'state-update-active', 'state-vacuum-active', 'state-valve-active', 'state-sensor-battery-high', 'state-sensor-battery-low', 'state-sensor-battery-medium', 'state-water_heater-eco', 'state-water_heater-electric', 'state-water_heater-gas', 'state-water_heater-heat_pump', 'state-water_heater-high_demand', 'state-water_heater-performance', 'state-weather-clear_night', 'state-weather-cloudy', 'state-weather-exceptional', 'state-weather-fog', 'state-weather-hail', 'state-weather-lightning_rainy', 'state-weather-lightning', 'state-weather-partlycloudy', 'state-weather-pouring', 'state-weather-rainy', 'state-weather-snowy_rainy', 'state-weather-snowy', 'state-weather-sunny', 'state-weather-windy_variant', 'state-weather-windy' ].map((c) => [c, `var(--${c}-color)`]), ), attributeMapping: { cover: { label: 'cover', attribute: 'current_position' }, light: { label: 'light', attribute: 'brightness' }, fan: { label: 'fan', attribute: 'percentage' }, }, numberFormat: { decimal_comma: 'de-DE', // 1.234,56 (Allemagne, France, etc.) comma_decimal: 'en-US', // 1,234.56 (USA, UK, etc.) space_comma: 'fr-FR', // 1 234,56 (France, Norvège, etc.) quote_decimal: 'de-CH', // 12'345.60 (Switzerland) }, entity: { state: { unavailable: 'unavailable', unknown: 'unknown', notFound: 'notFound', idle: 'idle', active: 'active', paused: 'paused' }, type: { timer: 'timer', light: 'light', cover: 'cover', fan: 'fan', climate: 'climate', counter: 'counter', number: 'number', duration: 'duration', default: 'default'}, class: { shutter: 'shutter', battery: 'battery' }, }, actions: { default: 'default', navigate: { action: 'navigate' }, moreInfo: { action: 'more-info' }, url: { action: 'url' }, assist: { action: 'assist' }, toggle: { action: 'toggle' }, performAction: { action: 'perform-action' }, none: { action: 'none' }, toggleDomain: [ 'light', 'switch', 'fan', 'input_boolean', 'media_player', 'automation', 'humidifier', 'remote', 'siren', 'water_heater', 'vacuum', 'group', ], }, styles: { rowSize: '--row-size', }, } const CARD = { config: { language: 'en', value: { min: 0, max: 100 }, unit: { default: '%', fahrenheit: '°F', timer: 'timer', flexTimer: 'flextimer', second: 's', disable: '', space: ' ', // HA dont use '\u202F' unitSpacing: { auto: 'auto', space: 'space', noSpace: 'no-space' }, }, showMoreInfo: true, reverse: false, decimal: { percentage: 0, timer: 0, counter: 0, duration: 0, other: 2 }, msFactor: 1000, shadowMode: 'open', refresh: { ratio: 500, min: 250, max: 1000 }, stub: { template: { icon: HA_CONTEXT.icons.washingMachine, name: META.types.card.name, secondary: 'Template', badge_icon: HA_CONTEXT.icons.update, badge_color: 'green', percent: '{{ 50 }}', force_circular_background: true, }, }, separator: ' · ', configError: 'Invalid config', }, htmlStructure: { card: { element: 'ha-card', extraAttr: { role: 'region', tabindex: '0', 'aria-label': META.types.card.name, }, }, sections: { container: { element: 'div', class: 'container' }, ripple: { element: 'ha-ripple' }, belowContainer: { element: 'div', class: 'below-container' }, topContainer: { element: 'div', class: 'top-container' }, bottomContainer: { element: 'div', class: 'bottom-container' }, backgroundContainer: { element: 'div', class: 'background-container' }, icon: { element: 'div', class: 'icon-section', extraAttr: {'aria-hidden': 'true'} }, content: { element: 'div', class: 'content-section' }, }, elements: { icon: { element: 'div', class: 'icon' }, shape: { element: 'shape', class: 'shape' }, ellipsisWrapper: { element: 'div', class: 'ellipsis-wrapper' }, nameContent: { element: 'div', class: 'name' }, nameValue: { element: 'span', class: 'name-value' }, nameMain: { element: 'span', class: 'name-main', id: 'entity-name' }, nameExtra: { element: 'span', class: 'name-extra' }, trendIndicator: { container: { element: 'div', class: 'trend-indicator' }, icon: { element: 'ha-icon', class: 'trend-icon' }, }, secondaryInfo: { element: 'div', class: 'secondary-info' }, secondaryInfoWrapper: { element: 'div', class: 'secondary-info-wrapper' }, secondaryInfoValue: { element: 'span', class: 'secondary-info-value' }, secondaryInfoMain: { element: 'span', class: 'secondary-info-main', id: 'entity-value' }, secondaryInfoExtra: { element: 'span', class: 'secondary-info-extra' }, progressBar: { container: { element: 'div', class: 'bar-container', extraAttr: { role: 'progressbar', 'aria-valuemin': 0, 'aria-valuemax': 100, 'aria-valuenow': 0, 'aria-labelledby': 'entity-name', 'aria-describedby': 'entity-value', }, }, bar: { element: 'div', class: 'progress-bar', extraAttr: {'aria-hidden': 'true'} }, inner: { element: 'div', class: 'inner', extraAttr: {'aria-hidden': 'true'} }, zeroMark: { element: 'div', class: 'zero', extraAttr: {'aria-hidden': 'true'} }, lowWatermark: { element: 'div', class: 'low', extraAttr: {'aria-hidden': 'true'} }, highWatermark: { element: 'div', class: 'high', extraAttr: {'aria-hidden': 'true'} }, watermark: { class: 'progress-bar-wm' }, }, badge: { container: { element: 'div', class: 'badge', extraAttr: {'aria-hidden': 'true'} }, icon: { element: 'ha-icon', class: 'badge-icon' }, }, }, }, style: { element: 'style', color: { default: HA_CONTEXT.colors.stateIcon, disabled: HA_CONTEXT.colors.darkGrey, unavailable: HA_CONTEXT.colors.unavailable, notFound: HA_CONTEXT.colors.inactive, active: HA_CONTEXT.colors.active, coverActive: HA_CONTEXT.colors.coverActive, lightActive: '#FF890E', fanActive: HA_CONTEXT.colors.fanActive, battery: { low: HA_CONTEXT.colors.batteryLow, medium: HA_CONTEXT.colors.batteryMedium, high: HA_CONTEXT.colors.batteryHigh, }, climate: { dry: HA_CONTEXT.colors.climateDry, cool: HA_CONTEXT.colors.climateCool, heat: HA_CONTEXT.colors.climateHeat, fanOnly: HA_CONTEXT.colors.climateFanOnly, }, inactive: HA_CONTEXT.colors.inactive, }, icon: { default: { icon: HA_CONTEXT.icons.alert }, alert: { icon: HA_CONTEXT.icons.alertCircleOutline, color: '#0080ff', attribute: 'icon' }, notFound: { icon: HA_CONTEXT.icons.help }, badge: { default: { attribute: 'icon' }, unavailable: { icon: HA_CONTEXT.icons.exclamationThick, color: 'white', backgroundColor: HA_CONTEXT.colors.orange, attribute: 'icon' }, notFound: { icon: HA_CONTEXT.icons.exclamationThick, color: 'white', backgroundColor: HA_CONTEXT.colors.red, attribute: 'icon' }, timer: { active: { icon: HA_CONTEXT.icons.play, color: 'white', backgroundColor: HA_CONTEXT.colors.success, attribute: 'icon' }, paused: { icon: HA_CONTEXT.icons.pause, color: 'white', backgroundColor: HA_CONTEXT.colors.stateIcon, attribute: 'icon' }, }, }, }, bar: { sizeOptions: { small: { label: 'small', mdi: HA_CONTEXT.icons.sizeSmall }, medium: { label: 'medium', mdi: HA_CONTEXT.icons.sizeMedium }, large: { label: 'large', mdi: HA_CONTEXT.icons.sizeLarge }, xlarge: { label: 'xlarge', mdi: HA_CONTEXT.icons.sizeXLarge }, }, }, dynamic: { card: { minWidth: { var: '--card-min-width' }, height: { var: '--card-height' }, }, badge: { color: { var: '--badge-color', default: HA_CONTEXT.colors.orange }, backgroundColor: { var: '--badge-bgcolor', default: 'transparent' }, }, iconAndShape: { color: { var: '--icon-and-shape-color', default: HA_CONTEXT.colors.stateIcon }, icon: { size: { var: '--icon-size' } }, shape: { size: { var: '--shape-size' } }, }, haRipple: { color: { var: '--ha-ripple-color' }, }, progressBar: { color: { var: '--progress-bar-color', default: HA_CONTEXT.colors.stateIcon }, value: { var: '--progress-bar-value', default: '0' }, maxWidth: { var: '--progress-bar-max-width', default: null }, background: { var: '--epb-progress-bar-background-color' }, orientation: { rtl: 'rtl-orientation', ltr: 'ltr-orientation', up: 'up-orientation' }, effect: { radius: { label: 'radius', class: 'progress-bar-effect-radius' }, glass: { label: 'glass', class: 'progress-bar-effect-glass' }, gradient: { label: 'gradient', class: 'progress-bar-effect-gradient' }, gradientReverse: { label: 'gradient_reverse', class: 'progress-bar-effect-gradient-reverse' }, shimmer: { label: 'shimmer', class: 'progress-bar-effect-shimmer' }, shimmerReverse: { label: 'shimmer_reverse', class: 'progress-bar-effect-shimmer-reverse' }, }, centerZero: { class: 'center-zero' }, }, watermark: { low: { value: { var: '--low-watermark-value', default: 20 }, color: { var: '--low-watermark-color', default: 'red' } }, high: { value: { var: '--high-watermark-value', default: 80 }, color: { var: '--high-watermark-color', default: 'red' } }, lineSize: { var: '--watermark-line-size' }, opacity: { var: '--watermark-opacity-value' }, }, secondaryInfoError: { class: 'secondary-info-error' }, show: 'show', hide: 'hide', clickable: { card: 'clickable-card', icon: 'clickable-icon' }, hiddenComponent: { icon: { label: 'icon', class: 'hide-icon' }, shape: { label: 'shape', class: 'hide-shape' }, name: { label: 'name', class: 'hide-name' }, secondary_info: { label: 'secondary_info', class: 'hide-secondary-info' }, value: { label: 'value' }, progress_bar: { label: 'progress_bar', class: 'hide-progress-bar' }, }, frameless: { class: 'frameless' }, marginless: { class: 'marginless' }, }, }, layout: { orientations: { horizontal: { label: 'horizontal', grid: { grid_rows: 1, grid_min_rows: 1, grid_columns: 2, grid_min_columns: 2 }, mdi: HA_CONTEXT.icons.focusHorizontal, minHeight: '56px', }, vertical: { label: 'vertical', grid: { grid_rows: 2, grid_min_rows: 2, grid_columns: 1, grid_min_columns: 1 }, mdi: HA_CONTEXT.icons.focusVertical, minHeight: '120px', }, }, }, theme: { default: '**CUSTOM**', battery: { label: 'battery', icon: 'battery' }, customTheme: { expectedKeys: ['min', 'max'], colorKeys: ['color', 'icon_color', 'bar_color'], }, }, network: { ready: 'ws-ready', disconnected: 'ws-disconnected', }, }; CARD.config.defaults = { tap_action: HA_CONTEXT.actions.moreInfo, hold_action: HA_CONTEXT.actions.none, double_tap_action: HA_CONTEXT.actions.none, icon_tap_action: HA_CONTEXT.actions.none, icon_hold_action: HA_CONTEXT.actions.none, icon_double_tap_action: HA_CONTEXT.actions.none, unit: null, layout: CARD.layout.orientations.horizontal.label, decimal: null, force_circular_background: false, disable_unit: false, unit_spacing: CARD.config.unit.unitSpacing.auto, entity: null, attribute: null, icon: null, name: null, max_value_attribute: null, color: null, theme: null, custom_theme: null, interpolate: false, bar_size: CARD.style.bar.sizeOptions.small.label, bar_color: null, bar_effect: [], bar_orientation: null, reverse: null, min_value: CARD.config.value.min, max_value: CARD.config.value.max, hide: [], badge_icon: null, badge_color: null, name_info: null, custom_info: null, state_content: [], frameless: false, marginless: false, center_zero: false, watermark: { low: 20, low_color: 'red', high: 80, high_color: 'red', opacity: 0.8, type: 'blended', line_size: '1px', disable_low: false, disable_high: false, }, }; CARD.console = { message: `%c✨${META.types.card.typeName.toUpperCase()} ${VERSION} IS INSTALLED.`, css: 'color:orange; background-color:black; font-weight: bold;', link: ' For more details, check the README: https://github.com/francois-le-ko4la/lovelace-entity-progress-card', }; const THEME = { optimal_when_low: { linear: false, percent: true, style: [ { min: 0, max: 20, icon: null, color: HA_CONTEXT.colors.success }, { min: 20, max: 50, icon: null, color: HA_CONTEXT.colors.yellow }, { min: 50, max: 80, icon: null, color: HA_CONTEXT.colors.accent }, { min: 80, max: 100, icon: null, color: HA_CONTEXT.colors.red }, ], }, optimal_when_high: { linear: false, percent: true, style: [ { min: 0, max: 20, icon: null, color: HA_CONTEXT.colors.red }, { min: 20, max: 50, icon: null, color: HA_CONTEXT.colors.accent }, { min: 50, max: 80, icon: null, color: HA_CONTEXT.colors.yellow }, { min: 80, max: 100, icon: null, color: HA_CONTEXT.colors.success }, ], }, light: { linear: true, percent: true, style: [ { icon: HA_CONTEXT.icons.lightbulbOutline, color: '#4B4B4B' }, { icon: HA_CONTEXT.icons.lightbulbOutline, color: '#877F67' }, { icon: HA_CONTEXT.icons.lightbulb, color: '#C3B382' }, { icon: HA_CONTEXT.icons.lightbulb, color: '#FFE79E' }, { icon: HA_CONTEXT.icons.lightbulb, color: '#FFE79E' }, ], }, temperature: { linear: false, percent: false, style: [ { min: -50, max: -30, icon: HA_CONTEXT.icons.thermometer, color: HA_CONTEXT.colors.deepPurple }, { min: -30, max: -15, icon: HA_CONTEXT.icons.thermometer, color: HA_CONTEXT.colors.indigo }, { min: -15, max: -2, icon: HA_CONTEXT.icons.thermometer, color: HA_CONTEXT.colors.blue }, { min: -2, max: 2, icon: HA_CONTEXT.icons.thermometer, color: HA_CONTEXT.colors.lightBlue }, { min: 2, max: 8, icon: HA_CONTEXT.icons.thermometer, color: HA_CONTEXT.colors.cyan }, { min: 8, max: 16, icon: HA_CONTEXT.icons.thermometer, color: HA_CONTEXT.colors.teal }, { min: 16, max: 18, icon: HA_CONTEXT.icons.thermometer, color: HA_CONTEXT.colors.green }, { min: 18, max: 20, icon: HA_CONTEXT.icons.thermometer, color: HA_CONTEXT.colors.lightGreen }, { min: 20, max: 25, icon: HA_CONTEXT.icons.thermometer, color: HA_CONTEXT.colors.success }, { min: 25, max: 27, icon: HA_CONTEXT.icons.thermometer, color: HA_CONTEXT.colors.yellow }, { min: 27, max: 29, icon: HA_CONTEXT.icons.thermometer, color: HA_CONTEXT.colors.amber }, { min: 29, max: 34, icon: HA_CONTEXT.icons.thermometer, color: HA_CONTEXT.colors.deepOrange }, { min: 34, max: 100, icon: HA_CONTEXT.icons.thermometer, color: HA_CONTEXT.colors.red }, ], }, humidity: { linear: false, percent: true, style: [ { min: 0, max: 23, icon: HA_CONTEXT.icons.waterPercent, color: HA_CONTEXT.colors.red }, { min: 23, max: 30, icon: HA_CONTEXT.icons.waterPercent, color: HA_CONTEXT.colors.accent }, { min: 30, max: 40, icon: HA_CONTEXT.icons.waterPercent, color: HA_CONTEXT.colors.yellow }, { min: 40, max: 50, icon: HA_CONTEXT.icons.waterPercent, color: HA_CONTEXT.colors.success }, { min: 50, max: 60, icon: HA_CONTEXT.icons.waterPercent, color: HA_CONTEXT.colors.teal }, { min: 60, max: 65, icon: HA_CONTEXT.icons.waterPercent, color: 'var(--light-blue-color)' }, { min: 65, max: 80, icon: HA_CONTEXT.icons.waterPercent, color: HA_CONTEXT.colors.indigo }, { min: 80, max: 100, icon: HA_CONTEXT.icons.waterPercent, color: HA_CONTEXT.colors.deepPurple }, ], }, voc: { linear: false, percent: false, style: [ { min: 0, max: 300, icon: HA_CONTEXT.icons.airFilter, color: HA_CONTEXT.colors.success }, { min: 300, max: 500, icon: HA_CONTEXT.icons.airFilter, color: HA_CONTEXT.colors.yellow }, { min: 500, max: 3000, icon: HA_CONTEXT.icons.airFilter, color: HA_CONTEXT.colors.accent }, { min: 3000, max: 25000, icon: HA_CONTEXT.icons.airFilter, color: HA_CONTEXT.colors.red }, { min: 25000, max: 50000, icon: HA_CONTEXT.icons.airFilter, color: HA_CONTEXT.colors.deepPurple }, ], }, pm25: { linear: false, percent: false, style: [ { min: 0, max: 12, icon: HA_CONTEXT.icons.airFilter, color: HA_CONTEXT.colors.success }, { min: 12, max: 35, icon: HA_CONTEXT.icons.airFilter, color: HA_CONTEXT.colors.yellow }, { min: 35, max: 55, icon: HA_CONTEXT.icons.airFilter, color: HA_CONTEXT.colors.accent }, { min: 55, max: 150, icon: HA_CONTEXT.icons.airFilter, color: HA_CONTEXT.colors.red }, { min: 150, max: 200, icon: HA_CONTEXT.icons.airFilter, color: HA_CONTEXT.colors.deepPurple }, ], }, }; const SEV = { info: 'info', warning: 'warning', error: 'error', debug: 'debug', }; // prettier-ignore-end /* eslint-disable sonarjs/no-duplicate-string */ const TRANSLATIONS = { ar: { card: { msg: { appliedDefaultValue: 'تم تطبيق قيمة افتراضية تلقائيًا.', attributeNotFound: 'لم يتم العثور على الخاصية في Home Assistant.', discontinuousRange: 'النطاق المحدد غير متصل.', entityNotFound: 'لم يتم العثور على الكيان في Home Assistant.', invalidActionObject: 'كائن الإجراء غير صالح أو غير منظم بشكل صحيح.', invalidCustomThemeArray: 'يجب أن يكون السمة المخصصة عبارة عن مصفوفة.', invalidCustomThemeEntry: 'إدخال أو أكثر في السمة المخصصة غير صالحة.', invalidDecimal: 'يجب أن تكون القيمة رقمًا عشريًا صحيحًا.', invalidEntityId: 'معرّف الكيان غير صالح أو به خلل.', invalidEnumValue: 'القيمة المُقدمة ليست من الخيارات المسموح بها.', invalidIconType: 'نوع الأيقونة المحدد غير صالح أو غير معروف.', invalidMaxValue: 'القيمة القصوى غير صالحة أو أعلى من الحد المسموح به.', invalidMinValue: 'القيمة الدنيا غير صالحة أو أقل من الحدود المسموح بها.', invalidStateContent: 'محتوى الحالة غير صالح أو معيب.', invalidStateContentEntry: 'إدخال أو أكثر في محتوى الحالة غير صالحة.', invalidTheme: 'السمة المحددة غير معروفة. سيتم استخدام السمة الافتراضية.', invalidTypeArray: 'كان من المتوقع قيمة من نوع مصفوفة.', invalidTypeBoolean: 'كان من المتوقع قيمة من نوع منطقي.', invalidTypeNumber: 'كان من المتوقع قيمة من نوع رقم.', invalidTypeObject: 'كان من المتوقع قيمة من نوع كائن.', invalidTypeString: 'كان من المتوقع قيمة من نوع سلسلة.', invalidUnionType: 'القيمة لا تطابق أي نوع مسموح.', minGreaterThanMax: 'لا يمكن أن تكون القيمة الدنيا أكبر من القيمة القصوى.', missingActionKey: 'مفتاح مطلوب مفقود في كائن الإجراء.', missingColorProperty: 'خاصية اللون المطلوبة مفقودة.', missingRequiredProperty: 'خاصية مطلوبة مفقودة.' } }, editor: { title: { content: 'المحتوى', interaction: 'التفاعلات', theme: 'المظهر' }, field: { attribute: 'السمة', badge_color: 'لون الشارة', badge_icon: 'أيقونة الشارة', bar_color: 'لون الشريط', bar_effect: 'تأثير الشريط', bar_orientation: 'اتجاه الشريط', bar_position: 'موضع الشريط', bar_single_line: 'معلومات في سطر واحد (تراكب)', bar_size: 'حجم الشريط', center_zero: 'صفر في الوسط', color: 'اللون الأساسي', decimal: 'عشري', disable_unit: 'عرض الوحدة', double_tap_action: 'الإجراء عند النقر المزدوج', entity: 'الكيان', force_circular_background: 'فرض خلفية دائرية', hide: 'إخفاء', hold_action: 'الإجراء عند الضغط المطول', icon: 'أيقونة', icon_double_tap_action: 'الإجراء عند النقر المزدوج على الأيقونة', icon_hold_action: 'الإجراء عند الضغط المطول على الأيقونة', icon_tap_action: 'الإجراء عند النقر على الأيقونة', layout: 'تخطيط المحتوى', max_value: 'القيمة القصوى', max_value_attribute: 'السمة (max_value)', max_value_entity: 'استخدام الكيان للقيمة القصوى', min_value: 'القيمة الدنيا', name: 'الاسم', percent: 'النسبة المئوية', reverse_secondary_info_row: 'تبديل الشريط والنص', secondary: 'معلومات ثانوية', state_content: 'محتوى الحالة', tap_action: 'الإجراء عند النقر القصير', text_shadow: 'إضافة ظل للنص (overlay)', theme: 'السمة', toggle_icon: 'عرض الأيقونة', toggle_name: 'عرض الاسم', toggle_progress_bar: 'عرض شريط التقدم', toggle_secondary_info: 'عرض المعلومات الثانوية', toggle_value: 'عرض القيمة', unit: 'الوحدة', use_max_entity: 'استخدام الكيان للقيمة القصوى' }, option: { theme: { optimal_when_low: 'مثالي عند الانخفاض (CPU، RAM...)', optimal_when_high: 'مثالي عند الارتفاع (البطارية...)', light: 'الضوء', temperature: 'درجة الحرارة', humidity: 'الرطوبة', pm25: 'PM2.5', voc: 'VOC' }, bar_size: { small: 'صغيرة', medium: 'متوسطة', large: 'كبيرة', xlarge: 'كبيرة جدًا' }, bar_orientation: { ltr: 'من اليسار إلى اليمين', rtl: 'من اليمين إلى اليسار', up: 'اتجاه لأعلى (تراكب)' }, bar_position: { default: 'افتراضي', below: 'الشريط تحت المحتوى', top: 'الشريط أعلى المحتوى (تراكب)', bottom: 'الشريط أسفل المحتوى (تراكب)', overlay: 'الشريط فوق المحتوى (تراكب)', background: 'خلفية البطاقة' }, layout: { horizontal: 'أفقي (افتراضي)', vertical: 'رأسي' } } } }, bn: { card: { msg: { appliedDefaultValue: 'ডিফল্ট মান স্বয়ংক্রিয়ভাবে প্রয়োগ করা হয়েছে।', attributeNotFound: 'HA তে বৈশিষ্ট্য পাওয়া যায়নি।', discontinuousRange: 'নির্ধারিত পরিসর অবিচ্ছিন্ন নয়।', entityNotFound: 'HA তে সত্তা পাওয়া যায়নি।', invalidActionObject: 'অ্যাকশন অবজেক্ট অবৈধ বা ভুলভাবে গঠিত।', invalidCustomThemeArray: 'কাস্টম থিম একটি অ্যারে হতে হবে।', invalidCustomThemeEntry: 'কাস্টম থিমে একটি বা একাধিক এন্ট্রি অবৈধ।', invalidDecimal: 'মানটি একটি বৈধ দশমিক সংখ্যা হতে হবে।', invalidEntityId: 'সত্তার আইডি অবৈধ বা ভুলভাবে গঠিত।', invalidEnumValue: 'প্রদত্ত মানটি অনুমোদিত বিকল্পগুলির মধ্যে একটি নয়।', invalidIconType: 'নির্দিষ্ট আইকন প্রকার অবৈধ বা অচেনা।', invalidMaxValue: 'সর্বোচ্চ মান অবৈধ বা অনুমোদিত সীমার উপরে।', invalidMinValue: 'ন্যূনতম মান অবৈধ বা অনুমোদিত সীমার নিচে।', invalidStateContent: 'অবস্থার বিষয়বস্তু অবৈধ বা ভুলভাবে গঠিত।', invalidStateContentEntry: 'অবস্থার বিষয়বস্তুতে একটি বা একাধিক এন্ট্রি অবৈধ।', invalidTheme: 'নির্দিষ্ট থিম অজানা। ডিফল্ট থিম ব্যবহার করা হবে।', invalidTypeArray: 'অ্যারে ধরনের একটি মান প্রত্যাশিত।', invalidTypeBoolean: 'বুলিয়ান ধরনের একটি মান প্রত্যাশিত।', invalidTypeNumber: 'সংখ্যা ধরনের একটি মান প্রত্যাশিত।', invalidTypeObject: 'অবজেক্ট ধরনের একটি মান প্রত্যাশিত।', invalidTypeString: 'স্ট্রিং ধরনের একটি মান প্রত্যাশিত।', invalidUnionType: 'মানটি অনুমোদিত ধরনগুলির কোনোটির সাথে মেলে না।', minGreaterThanMax: 'ন্যূনতম মান সর্বোচ্চ মানের চেয়ে বড় হতে পারে না।', missingActionKey: 'অ্যাকশন অবজেক্টে একটি প্রয়োজনীয় কী অনুপস্থিত।', missingColorProperty: 'একটি প্রয়োজনীয় রঙের বৈশিষ্ট্য অনুপস্থিত।', missingRequiredProperty: 'প্রয়োজনীয় বৈশিষ্ট্য অনুপস্থিত।' } }, editor: { title: { content: 'বিষয়বস্তু', interaction: 'মিথস্ক্রিয়া', theme: 'চেহারা এবং অনুভূতি' }, field: { attribute: 'বৈশিষ্ট্য', badge_color: 'ব্যাজের রঙ', badge_icon: 'ব্যাজ আইকন', bar_color: 'বারের রঙ', bar_effect: 'বারের প্রভাব', bar_orientation: 'বারের অভিমুখ', bar_position: 'বারের অবস্থান', bar_single_line: 'এক লাইনে তথ্য (ওভারলে)', bar_size: 'বারের আকার', center_zero: 'মাঝে শূন্য', color: 'প্রাথমিক রঙ', decimal: 'দশমিক', disable_unit: 'একক দেখান', double_tap_action: 'ডাবল ট্যাপ আচরণ', entity: 'সত্তা', force_circular_background: 'বৃত্তাকার পটভূমি জোর করুন', hide: 'লুকান', hold_action: 'হোল্ড আচরণ', icon: 'আইকন', icon_double_tap_action: 'আইকন ডাবল ট্যাপ আচরণ', icon_hold_action: 'আইকন হোল্ড আচরণ', icon_tap_action: 'আইকন ট্যাপ আচরণ', layout: 'বিষয়বস্তুর বিন্যাস', max_value: 'সর্বোচ্চ মান', max_value_attribute: 'বৈশিষ্ট্য (max_value)', max_value_entity: 'সত্তার সর্বোচ্চ মান', min_value: 'ন্যূনতম মান', name: 'নাম', percent: 'শতাংশ', reverse_secondary_info_row: 'বার এবং টেক্সট অদলবদল করুন', secondary: 'দ্বিতীয় তথ্য', state_content: 'স্টেটের বিষয়বস্তু', tap_action: 'ট্যাপ আচরণ', text_shadow: 'টেক্সটে ছায়া যোগ করুন (overlay)', theme: 'থিম', toggle_icon: 'আইকন', toggle_name: 'নাম', toggle_progress_bar: 'বার', toggle_secondary_info: 'তথ্য', toggle_value: 'মান', unit: 'একক', use_max_entity: 'সর্বোচ্চ মানের জন্য সত্তা ব্যবহার করুন' }, option: { theme: { optimal_when_low: 'কম হলে সর্বোত্তম (CPU, RAM,...)', optimal_when_high: 'বেশি হলে সর্বোত্তম (ব্যাটারি...)', light: 'আলো', temperature: 'তাপমাত্রা', humidity: 'আর্দ্রতা', pm25: 'PM2.5', voc: 'VOC' }, bar_size: { small: 'ছোট', medium: 'মাঝারি', large: 'বড়', xlarge: 'অতিরিক্ত বড়' }, bar_orientation: { ltr: 'বাম থেকে ডানে', rtl: 'ডান থেকে বামে', up: 'উপরের দিকে (ওভারলে)' }, bar_position: { default: 'ডিফল্ট', below: 'বিষয়বস্তুর নিচে বার', top: 'উপরের দিকে বার (ওভারলে)', bottom: 'নিচের দিকে বার (ওভারলে)', overlay: 'বিষয়বস্তুর ওপর বার (ওভারলে)', background: 'কার্ড পটভূমি' }, layout: { horizontal: 'অনুভূমিক (ডিফল্ট)', vertical: 'উল্লম্ব' } } } }, ca: { card: { msg: { appliedDefaultValue: 'S\'ha aplicat automàticament un valor per defecte.', attributeNotFound: 'No s\'ha trobat l\'atribut a Home Assistant.', discontinuousRange: 'El rang definit és discontinu.', entityNotFound: 'No s\'ha trobat l\'entitat a Home Assistant.', invalidActionObject: 'L\'objecte d\'acció és invàlid o té una estructura incorrecta.', invalidCustomThemeArray: 'El tema personalitzat ha de ser un array.', invalidCustomThemeEntry: 'Una o més entrades del tema personalitzat són invàlides.', invalidDecimal: 'El valor ha de ser un decimal vàlid.', invalidEntityId: 'L\'ID de l\'entitat no és vàlid o té un format incorrecte.', invalidEnumValue: 'El valor proporcionat no és una opció vàlida.', invalidIconType: 'El tipus d\'icona especificat és invàlid o desconegut.', invalidMaxValue: 'El valor màxim és invàlid o supera el límit permès.', invalidMinValue: 'El valor mínim és invàlid o està per sota del límit permès.', invalidStateContent: 'El contingut de l\'estat és invàlid o té un format incorrecte.', invalidStateContentEntry: 'Una o més entrades del contingut de l\'estat són invàlides.', invalidTheme: 'El tema especificat és desconegut. S\'utilitzarà el tema per defecte.', invalidTypeArray: 'S\'esperava un valor de tipus array.', invalidTypeBoolean: 'S\'esperava un valor de tipus boolean.', invalidTypeNumber: 'S\'esperava un valor de tipus número.', invalidTypeObject: 'S\'esperava un valor de tipus objecte.', invalidTypeString: 'S\'esperava un valor de tipus cadena.', invalidUnionType: 'El valor no coincideix amb cap dels tipus permesos.', minGreaterThanMax: 'El valor mínim no pot ser més gran que el valor màxim.', missingActionKey: 'Falta una clau obligatòria a l\'objecte d\'acció.', missingColorProperty: 'Falta una propietat de color obligatòria.', missingRequiredProperty: 'Falta una propietat obligatòria.' } }, editor: { title: { content: 'Contingut', interaction: 'Interacció', theme: 'Aparença i tema' }, field: { attribute: 'Atribut', badge_color: 'Color de la insígnia', badge_icon: 'Icona de la insígnia', bar_color: 'Color principal', bar_effect: 'Efecte de la barra', bar_orientation: 'Orientació de la barra', bar_position: 'Posició de la barra', bar_single_line: 'Informació en una sola línia (overlay)', bar_size: 'Mida de la barra', center_zero: 'Zero al centre', color: 'Color principal', decimal: 'Decimal', disable_unit: 'Mostra la unitat', double_tap_action: 'Acció al doble tocar', entity: 'Entitat', force_circular_background: 'Forçar fons circular', hide: 'Amaga', hold_action: 'Acció en mantenir premut', icon: 'Icona', icon_double_tap_action: 'Acció al doble tocar la icona', icon_hold_action: 'Acció en mantenir premuda la icona', icon_tap_action: 'Acció al tocar la icona', layout: 'Disposició del contingut', max_value: 'Valor màxim', max_value_attribute: 'Atribut (valor màxim)', max_value_entity: 'Usar entitat com a valor màxim', min_value: 'Valor mínim', name: 'Nom', percent: 'Percentatge', reverse_secondary_info_row: 'Intercanvia barra i text', secondary: 'Informació secundària', state_content: 'Contingut de l\'estat', tap_action: 'Acció al tocar breument', text_shadow: 'Afegir ombra al text (overlay)', theme: 'Tema', toggle_icon: 'Mostra icona', toggle_name: 'Mostra nom', toggle_progress_bar: 'Mostra barra de progrés', toggle_secondary_info: 'Mostra informació secundària', toggle_value: 'Mostra valor', unit: 'Unitat', use_max_entity: 'Usar una entitat com a valor màxim' }, option: { theme: { optimal_when_low: 'Òptim quan és baix (CPU, RAM…)', optimal_when_high: 'Òptim quan és alt (Bateria…)', light: 'Llum', temperature: 'Temperatura', humidity: 'Humitat', pm25: 'PM2.5', voc: 'VOC' }, bar_size: { small: 'Petita', medium: 'Mitjana', large: 'Gran', xlarge: 'Extra gran' }, bar_orientation: { ltr: 'D\'esquerra a dreta', rtl: 'De dreta a esquerra', up: 'Cap amunt (overlay)' }, bar_position: { default: 'Predeterminada', below: 'Barra sota el contingut', top: 'Barra a sobre (superposada)', bottom: 'Barra a sota (superposada)', overlay: 'Barra superposada al contingut (overlay)', background: 'Fons de la targeta' }, layout: { horizontal: 'Horitzontal (predeterminada)', vertical: 'Vertical' } } } }, cs: { card: { msg: { appliedDefaultValue: 'Výchozí hodnota byla automaticky aplikována.', attributeNotFound: 'Atribut nebyl nalezen v HA.', discontinuousRange: 'Definovaný rozsah je nespojitý.', entityNotFound: 'Entita nebyla nalezena v HA.', invalidActionObject: 'Objekt akce je neplatný nebo špatně strukturovaný.', invalidCustomThemeArray: 'Vlastní motiv musí být pole.', invalidCustomThemeEntry: 'Jedna nebo více položek ve vlastním motivu je neplatných.', invalidDecimal: 'Hodnota musí být platné desítkové číslo.', invalidEntityId: 'ID entity je neplatné nebo špatně formátované.', invalidEnumValue: 'Poskytnutá hodnota není jednou z povolených možností.', invalidIconType: 'Zadaný typ ikony je neplatný nebo nerozpoznaný.', invalidMaxValue: 'Maximální hodnota je neplatná nebo nad povolenými limity.', invalidMinValue: 'Minimální hodnota je neplatná nebo pod povolenými limity.', invalidStateContent: 'Obsah stavu je neplatný nebo špatně formátovaný.', invalidStateContentEntry: 'Jedna nebo více položek v obsahu stavu je neplatných.', invalidTheme: 'Zadaný motiv je neznámý. Bude použit výchozí motiv.', invalidTypeArray: 'Očekávána hodnota typu pole.', invalidTypeBoolean: 'Očekávána hodnota typu boolean.', invalidTypeNumber: 'Očekávána hodnota typu číslo.', invalidTypeObject: 'Očekávána hodnota typu objekt.', invalidTypeString: 'Očekávána hodnota typu řetězec.', invalidUnionType: 'Hodnota neodpovídá žádnému z povolených typů.', minGreaterThanMax: 'Minimální hodnota nemůže být větší než maximální hodnota.', missingActionKey: 'V objektu akce chybí požadovaný klíč.', missingColorProperty: 'Chybí povinná vlastnost barvy.', missingRequiredProperty: 'Chybí povinná vlastnost.' } }, editor: { title: { content: 'Obsah', interaction: 'Interakce', theme: 'Vzhled a pocit' }, field: { attribute: 'Atribut', badge_color: 'Barva odznaku', badge_icon: 'Ikona odznaku', bar_color: 'Barva lišty', bar_effect: 'Efekt na liště', bar_orientation: 'Orientace lišty', bar_position: 'Umístění lišty', bar_single_line: 'Info v jednom řádku (overlay)', bar_size: 'Velikost lišty', center_zero: 'Nula uprostřed', color: 'Hlavní barva', decimal: 'desetinný', disable_unit: 'Zobrazit jednotku', double_tap_action: 'Chování při dvojitém klepnutí', entity: 'Entita', force_circular_background: 'Vynutit kruhové pozadí', hide: 'Skrýt', hold_action: 'Chování při podržení', icon: 'Ikona', icon_double_tap_action: 'Chování při dvojitém klepnutí na ikonu', icon_hold_action: 'Chování při podržení ikony', icon_tap_action: 'Chování při klepnutí na ikonu', layout: 'Rozložení obsahu', max_value: 'Maximální hodnota', max_value_attribute: 'Atribut (max_value)', max_value_entity: 'Použít entitu pro maximální hodnotu', min_value: 'Minimální hodnota', name: 'Název', percent: 'Procento', reverse_secondary_info_row: 'Zaměnit lištu a text', secondary: 'Sekundární informace', state_content: 'Obsah stavu', tap_action: 'Chování při klepnutí', text_shadow: 'Přidat stín textu (overlay)', theme: 'Motiv', toggle_icon: 'Zobrazit ikonu', toggle_name: 'Zobrazit název', toggle_progress_bar: 'Zobrazit lištu postupu', toggle_secondary_info: 'Zobrazit sekundární informace', toggle_value: 'Zobrazit hodnotu', unit: 'Jednotka', use_max_entity: 'Použít entitu pro max hodnotu' }, option: { theme: { optimal_when_low: 'Optimální při nízkých hodnotách (CPU, RAM...)', optimal_when_high: 'Optimální při vysokých hodnotách (Baterie...)', light: 'Světlo', temperature: 'Teplota', humidity: 'Vlhkost', pm25: 'PM2.5', voc: 'VOC' }, bar_size: { small: 'Malá', medium: 'Střední', large: 'Velká', xlarge: 'Extra velká' }, bar_orientation: { ltr: 'Zleva doprava', rtl: 'Zprava doleva', up: 'Nahoru (overlay)' }, bar_position: { default: 'Výchozí', below: 'Lišta pod obsahem', top: 'Lišta nahoře (overlay)', bottom: 'Lišta dole (overlay)', overlay: 'Lišta přes obsah (overlay)', background: 'Pozadí karty' }, layout: { horizontal: 'Horizontální (výchozí)', vertical: 'Vertikální' } } } }, da: { card: { msg: { appliedDefaultValue: 'Standardværdi er blevet anvendt automatisk.', attributeNotFound: 'Egenskab blev ikke fundet i Home Assistant.', discontinuousRange: 'Det definerede interval er usammenhængende.', entityNotFound: 'Enheden blev ikke fundet i Home Assistant.', invalidActionObject: 'Handlingsobjektet er ugyldigt eller forkert struktureret.', invalidCustomThemeArray: 'Det brugerdefinerede tema skal være en array.', invalidCustomThemeEntry: 'En eller flere indgange i det brugerdefinerede tema er ugyldige.', invalidDecimal: 'Værdien skal være et gyldigt decimaltal.', invalidEntityId: 'Enheds-ID er ugyldigt eller forkert formateret.', invalidEnumValue: 'Den angivne værdi er ikke en tilladt mulighed.', invalidIconType: 'Den angivne ikontype er ugyldig eller ukendt.', invalidMaxValue: 'Maksimumværdi er ugyldig eller overstiger den tilladte grænse.', invalidMinValue: 'Mindsteværdi er ugyldig eller under den tilladte grænse.', invalidStateContent: 'Tilstandsindholdet er ugyldigt eller fejlbehæftet.', invalidStateContentEntry: 'En eller flere poster i tilstandsindholdet er ugyldige.', invalidTheme: 'Det angivne tema er ukendt. Standardtema anvendes.', invalidTypeArray: 'Forventede en array-værdi.', invalidTypeBoolean: 'Forventede en boolesk værdi.', invalidTypeNumber: 'Forventede en numerisk værdi.', invalidTypeObject: 'Forventede en objektværdi.', invalidTypeString: 'Forventede en strengværdi.', invalidUnionType: 'Værdien matcher ingen af de tilladte typer.', minGreaterThanMax: 'Mindsteværdi kan ikke være større end maksimumværdi.', missingActionKey: 'En påkrævet nøgle mangler i handlingsobjektet.', missingColorProperty: 'En påkrævet farveegenskab mangler.', missingRequiredProperty: 'En påkrævet egenskab mangler.' } }, editor: { title: { content: 'Indhold', interaction: 'Interaktioner', theme: 'Udseende og funktionalitet' }, field: { attribute: 'Attribut', badge_color: 'Badge-farve', badge_icon: 'Badge-ikon', bar_color: 'Farve til bar', bar_effect: 'Effekt på bar', bar_orientation: 'Bar-retning', bar_position: 'Bar-placering', bar_single_line: 'Info på én linje (overlay)', bar_size: 'Bar størrelse', center_zero: 'Center nul', color: 'Primær farve', decimal: 'decimal', disable_unit: 'Vis enhed', double_tap_action: 'Handling ved dobbelt tryk', entity: 'Enhed', force_circular_background: 'Tving cirkulær baggrund', hide: 'Skjul', hold_action: 'Handling ved langt tryk', icon: 'Ikon', icon_double_tap_action: 'Handling ved dobbelt tryk på ikonet', icon_hold_action: 'Handling ved langt tryk på ikonet', icon_tap_action: 'Handling ved tryk på ikonet', layout: 'Indholdslayout', max_value: 'Maksimal værdi', max_value_attribute: 'Attribut (max_value)', max_value_entity: 'Brug enhed for max-værdi', min_value: 'Minimumsværdi', name: 'Navn', percent: 'Procent', reverse_secondary_info_row: 'Skift bjælke og tekst', secondary: 'Sekundær info', state_content: 'Indhold af tilstand', tap_action: 'Handling ved kort tryk', text_shadow: 'Tilføj tekstskygge (overlay)', theme: 'Tema', toggle_icon: 'Vis ikon', toggle_name: 'Vis navn', toggle_progress_bar: 'Vis fremdriftsbar', toggle_secondary_info: 'Vis sekundær info', toggle_value: 'Vis værdi', unit: 'Enhed', use_max_entity: 'Brug enhed for max-værdi' }, option: { theme: { optimal_when_low: 'Optimal når lavt (CPU, RAM,...)', optimal_when_high: 'Optimal når højt (Batteri...)', light: 'Lys', temperature: 'Temperatur', humidity: 'Fugtighed', pm25: 'PM2.5', voc: 'VOC' }, bar_size: { small: 'Lille', medium: 'Medium', large: 'Stor', xlarge: 'Ekstra stor' }, bar_orientation: { ltr: 'Venstre til højre', rtl: 'Højre til venstre', up: 'Opad (overlay)' }, bar_position: { default: 'Standard', below: 'Bar under indhold', top: 'Bar øverst (overlay)', bottom: 'Bar nederst (overlay)', overlay: 'Bar over indhold (overlay)', background: 'Kortbaggrund' }, layout: { horizontal: 'Horisontal (standard)', vertical: 'Vertikal' } } } }, de: { card: { msg: { appliedDefaultValue: 'Ein Standardwert wurde automatisch angewendet.', attributeNotFound: 'Attribut in Home Assistant nicht gefunden.', discontinuousRange: 'Der definierte Bereich ist nicht kontinuierlich.', entityNotFound: 'Entität in Home Assistant nicht gefunden.', invalidActionObject: 'Das Aktionsobjekt ist ungültig oder falsch strukturiert.', invalidCustomThemeArray: 'Das benutzerdefinierte Theme muss ein Array sein.', invalidCustomThemeEntry: 'Ein oder mehrere Einträge im benutzerdefinierten Theme sind ungültig.', invalidDecimal: 'Der Wert muss eine gültige Dezimalzahl sein.', invalidEntityId: 'Die Entity-ID ist ungültig oder fehlerhaft.', invalidEnumValue: 'Der angegebene Wert gehört nicht zu den erlaubten Optionen.', invalidIconType: 'Der angegebene Symboltyp ist ungültig oder nicht erkannt.', invalidMaxValue: 'Der Maximalwert ist ungültig oder überschreitet den erlaubten Bereich.', invalidMinValue: 'Der Minimalwert ist ungültig oder liegt unterhalb des erlaubten Bereichs.', invalidStateContent: 'Der Statusinhalt ist ungültig oder fehlerhaft.', invalidStateContentEntry: 'Ein oder mehrere Einträge im Statusinhalt sind ungültig.', invalidTheme: 'Das angegebene Theme ist unbekannt. Das Standard-Theme wird verwendet.', invalidTypeArray: 'Ein Wert vom Typ Array wurde erwartet.', invalidTypeBoolean: 'Ein Wert vom Typ Boolesch wurde erwartet.', invalidTypeNumber: 'Ein Wert vom Typ Zahl wurde erwartet.', invalidTypeObject: 'Ein Wert vom Typ Objekt wurde erwartet.', invalidTypeString: 'Ein Wert vom Typ Zeichenkette wurde erwartet.', invalidUnionType: 'Der Wert entspricht keinem der erlaubten Typen.', minGreaterThanMax: 'Der Minimalwert darf nicht größer als der Maximalwert sein.', missingActionKey: 'Ein erforderlicher Schlüssel fehlt im Aktionsobjekt.', missingColorProperty: 'Eine erforderliche Farbeigenschaft fehlt.', missingRequiredProperty: 'Eine erforderliche Eigenschaft fehlt.' } }, editor: { title: { content: 'Inhalt', interaction: 'Interaktionen', theme: 'Aussehen und Bedienung' }, field: { attribute: 'Attribut', badge_color: 'Farbe des Badges', badge_icon: 'Symbol des Badges', bar_color: 'Farbe der Leiste', bar_effect: 'Effekt auf die Leiste', bar_orientation: 'Ausrichtung der Leiste', bar_position: 'Position der Leiste', bar_single_line: 'Informationen in einer Zeile (Overlay)', bar_size: 'Größe der Bar', center_zero: 'Null in der Mitte', color: 'Primärfarbe', decimal: 'dezimal', disable_unit: 'Einheit anzeigen', double_tap_action: 'Aktion bei doppelt Tippen', entity: 'Entität', force_circular_background: 'Kreisförmigen Hintergrund erzwingen', hide: 'Ausblenden', hold_action: 'Aktion bei langem Tippen', icon: 'Symbol', icon_double_tap_action: 'Aktion bei doppelt Tippen auf das Symbol', icon_hold_action: 'Aktion bei langem Tippen auf das Symbol', icon_tap_action: 'Aktion beim Tippen auf das Symbol', layout: 'Inhaltslayout', max_value: 'Höchstwert', max_value_attribute: 'Attribut (max_value)', max_value_entity: 'Entität für Maximalwert verwenden', min_value: 'Mindestwert', name: 'Name', percent: 'Prozent', reverse_secondary_info_row: 'Barra und Text tauschen', secondary: 'Sekundäre Informationen', state_content: 'Statusinhalt', tap_action: 'Aktion bei kurzem Tippen', text_shadow: 'Textschatten hinzufügen (Overlay)', theme: 'Thema', toggle_icon: 'Icon anzeigen', toggle_name: 'Name anzeigen', toggle_progress_bar: 'Fortschrittsbalken anzeigen', toggle_secondary_info: 'Sekundärinformationen anzeigen', toggle_value: 'Wert anzeigen', unit: 'Einheit', use_max_entity: 'Entität für Maximalwert verwenden' }, option: { theme: { optimal_when_low: 'Optimal bei niedrig (CPU, RAM,...)', optimal_when_high: 'Optimal bei hoch (Batterie...)', light: 'Licht', temperature: 'Temperatur', humidity: 'Feuchtigkeit', pm25: 'PM2.5', voc: 'VOC' }, bar_size: { small: 'Klein', medium: 'Mittel', large: 'Groß', xlarge: 'Extra groß' }, bar_orientation: { ltr: 'Von links nach rechts', rtl: 'Von rechts nach links', up: 'Nach oben (Overlay)' }, bar_position: { default: 'Standard', below: 'Leiste unter dem Inhalt', top: 'Leiste oben (Overlay)', bottom: 'Leiste unten (Overlay)', overlay: 'Leiste über dem Inhalt (Overlay)', background: 'Kartenhintergrund' }, layout: { horizontal: 'Horizontal (Standard)', vertical: 'Vertikal' } } } }, el: { card: { msg: { appliedDefaultValue: 'Εφαρμόστηκε αυτόματα προεπιλεγμένη τιμή.', attributeNotFound: 'Το χαρακτηριστικό δεν βρέθηκε στο Home Assistant.', discontinuousRange: 'Το καθορισμένο εύρος δεν είναι συνεχές.', entityNotFound: 'Η οντότητα δεν βρέθηκε στο Home Assistant.', invalidActionObject: 'Το αντικείμενο ενέργειας δεν είναι έγκυρο ή είναι κακώς δομημένο.', invalidCustomThemeArray: 'Το προσαρμοσμένο θέμα πρέπει να είναι πίνακας.', invalidCustomThemeEntry: 'Μία ή περισσότερες καταχωρήσεις στο προσαρμοσμένο θέμα δεν είναι έγκυρες.', invalidDecimal: 'Η τιμή πρέπει να είναι έγκυρος δεκαδικός αριθμός.', invalidEntityId: 'Το αναγνωριστικό οντότητας δεν είναι έγκυρο ή είναι κακώς διαμορφωμένο.', invalidEnumValue: 'Η παρεχόμενη τιμή δεν είναι αποδεκτή επιλογή.', invalidIconType: 'Ο καθορισμένος τύπος εικονιδίου δεν είναι έγκυρος ή αναγνωρίσιμος.', invalidMaxValue: 'Η μέγιστη τιμή δεν είναι έγκυρη ή ξεπερνά τα όρια.', invalidMinValue: 'Η ελάχιστη τιμή δεν είναι έγκυρη ή είναι εκτός επιτρεπόμενων ορίων.', invalidStateContent: 'Το περιεχόμενο κατάστασης δεν είναι έγκυρο ή είναι κακώς διαμορφωμένο.', invalidStateContentEntry: 'Μία ή περισσότερες καταχωρήσεις στο περιεχόμενο κατάστασης είναι άκυρες.', invalidTheme: 'Το καθορισμένο θέμα είναι άγνωστο. Θα χρησιμοποιηθεί το προεπιλεγμένο θέμα.', invalidTypeArray: 'Αναμενόταν τιμή τύπου πίνακα.', invalidTypeBoolean: 'Αναμενόταν τιμή τύπου boolean.', invalidTypeNumber: 'Αναμενόταν τιμή τύπου αριθμού.', invalidTypeObject: 'Αναμενόταν τιμή τύπου αντικειμένου.', invalidTypeString: 'Αναμενόταν τιμή τύπου συμβολοσειράς.', invalidUnionType: 'Η τιμή δεν ταιριάζει σε κανέναν από τους επιτρεπόμενους τύπους.', minGreaterThanMax: 'Η ελάχιστη τιμή δεν μπορεί να είναι μεγαλύτερη από τη μέγιστη.', missingActionKey: 'Λείπει απαιτούμενο κλειδί στο αντικείμενο ενέργειας.', missingColorProperty: 'Λείπει απαιτούμενη ιδιότητα χρώματος.', missingRequiredProperty: 'Λείπει μια απαιτούμενη ιδιότητα.' } }, editor: { title: { content: 'Περιεχόμενο', interaction: 'Αλληλεπιδράσεις', theme: 'Εμφάνιση' }, field: { attribute: 'Χαρακτηριστικό', badge_color: 'Χρώμα εμβλήματος', badge_icon: 'Εικονίδιο εμβλήματος', bar_color: 'Χρώμα γραμμής', bar_effect: 'Εφέ γραμμής', bar_orientation: 'Κατεύθυνση γραμμής', bar_position: 'Θέση γραμμής', bar_single_line: 'Πληροφορίες σε μία γραμμή (overlay)', bar_size: 'Μέγεθος γραμμής', center_zero: 'Μηδέν στο κέντρο', color: 'Κύριο χρώμα', decimal: 'δεκαδικά', disable_unit: 'Εμφάνιση μονάδας', double_tap_action: 'Ενέργεια κατά το διπλό πάτημα', entity: 'Οντότητα', force_circular_background: 'Εξαναγκασμός κυκλικού φόντου', hide: 'Απόκρυψη', hold_action: 'Ενέργεια κατά το παρατεταμένο πάτημα', icon: 'Εικονίδιο', icon_double_tap_action: 'Ενέργεια στο διπλό πάτημα του εικονιδίου', icon_hold_action: 'Ενέργεια στο παρατεταμένο πάτημα του εικονιδίου', icon_tap_action: 'Ενέργεια στο πάτημα του εικονιδίου', layout: 'Διάταξη περιεχομένου', max_value: 'Μέγιστη τιμή', max_value_attribute: 'Χαρακτηριστικό (max_value)', max_value_entity: 'Χρήση οντότητας ως μέγιστης τιμής', min_value: 'Ελάχιστη τιμή', name: 'Όνομα', percent: 'Ποσοστό', reverse_secondary_info_row: 'Εναλλαγή γραμμής και κειμένου', secondary: 'Πρόσθετες πληροφορίες', state_content: 'Περιεχόμενο κατάστασης', tap_action: 'Ενέργεια κατά το σύντομο πάτημα', text_shadow: 'Προσθήκη σκιάς στο κείμενο (overlay)', theme: 'Θέμα', toggle_icon: 'Εμφάνιση εικονιδίου', toggle_name: 'Εμφάνιση ονόματος', toggle_progress_bar: 'Εμφάνιση γραμμής προόδου', toggle_secondary_info: 'Εμφάνιση πρόσθετων πληροφοριών', toggle_value: 'Εμφάνιση τιμής', unit: 'Μονάδα', use_max_entity: 'Χρήση οντότητας ως μέγιστης τιμής' }, option: { theme: { optimal_when_low: 'Βέλτιστο όταν είναι χαμηλό (CPU, RAM...)', optimal_when_high: 'Βέλτιστο όταν είναι υψηλό (Μπαταρία...)', light: 'Φωτεινότητα', temperature: 'Θερμοκρασία', humidity: 'Υγρασία', pm25: 'PM2.5', voc: 'VOC' }, bar_size: { small: 'Μικρή', medium: 'Μεσαία', large: 'Μεγάλη', xlarge: 'Πολύ μεγάλη' }, bar_orientation: { ltr: 'Από αριστερά προς δεξιά', rtl: 'Από δεξιά προς αριστερά', up: 'Προς τα πάνω (overlay)' }, bar_position: { default: 'Προεπιλογή', below: 'Γραμμή κάτω από το περιεχόμενο', top: 'Γραμμή πάνω (επικάλυψη)', bottom: 'Γραμμή κάτω (επικάλυψη)', overlay: 'Γραμμή πάνω από περιεχόμενο (overlay)', background: 'Φόντο κάρτας' }, layout: { horizontal: 'Οριζόντια (προεπιλογή)', vertical: 'Κατακόρυφη' } } } }, en: { card: { msg: { appliedDefaultValue: 'A default value has been applied automatically.', attributeNotFound: 'Attribute not found in HA.', discontinuousRange: 'The defined range is discontinuous.', entityNotFound: 'Entity not found in HA.', invalidActionObject: 'The action object is invalid or improperly structured.', invalidCustomThemeArray: 'The custom theme must be an array.', invalidCustomThemeEntry: 'One or more entries in the custom theme are invalid.', invalidDecimal: 'The value must be a valid decimal number.', invalidEntityId: 'The entity ID is invalid or malformed.', invalidEnumValue: 'The provided value is not one of the allowed options.', invalidIconType: 'The specified icon type is invalid or unrecognized.', invalidMaxValue: 'The maximum value is invalid or above allowed limits.', invalidMinValue: 'The minimum value is invalid or below allowed limits.', invalidStateContent: 'The state content is invalid or malformed.', invalidStateContentEntry: 'One or more entries in the state content are invalid.', invalidTheme: 'The specified theme is unknown. Default theme will be used.', invalidTypeArray: 'Expected a value of type array.', invalidTypeBoolean: 'Expected a value of type boolean.', invalidTypeNumber: 'Expected a value of type number.', invalidTypeObject: 'Expected a value of type object.', invalidTypeString: 'Expected a value of type string.', invalidUnionType: 'The value does not match any of the allowed types.', minGreaterThanMax: 'Minimum value cannot be greater than maximum value.', missingActionKey: 'A required key is missing in the action object.', missingColorProperty: 'A required color property is missing.', missingRequiredProperty: 'Required property is missing.' } }, editor: { title: { content: 'Content', interaction: 'Interactions', theme: 'Look & Feel' }, field: { attribute: 'Attribute', badge_color: 'Badge color', badge_icon: 'Badge icon', bar_color: 'Color for the bar', bar_effect: 'Bar effect', bar_orientation: 'Bar orientation', bar_position: 'Bar position', bar_single_line: 'Single line info (overlay)', bar_size: 'Bar size', center_zero: 'Zero at center', color: 'Primary color', decimal: 'decimal', disable_unit: 'Show unit', double_tap_action: 'Double tap behavior', entity: 'Entity', force_circular_background: 'Force icon circular background', hide: 'Hide', hold_action: 'Hold behavior', icon: 'Icon', icon_double_tap_action: 'Icon double tap behavior', icon_hold_action: 'Icon hold behavior', icon_tap_action: 'Icon tap behavior', layout: 'Content layout', max_value: 'Maximum value', max_value_attribute: 'Attribute (max_value)', max_value_entity: 'Use entity as maximum value', min_value: 'Minimum value', name: 'Name', percent: 'Percentage', reverse_secondary_info_row: 'Swap bar and text', secondary: 'Secondary info', state_content: 'State content', tap_action: 'Tap behavior', text_shadow: 'Add text shadow (overlay)', theme: 'Theme', toggle_icon: 'Show icon', toggle_name: 'Show name', toggle_progress_bar: 'Show progress bar', toggle_secondary_info: 'Show secondary info', toggle_value: 'Show value', unit: 'Unit', use_max_entity: 'Use entity for max value' }, option: { theme: { optimal_when_low: 'Optimal when Low (CPU, RAM,...)', optimal_when_high: 'Optimal when High (Battery...)', light: 'Light', temperature: 'Temperature', humidity: 'Humidity', pm25: 'PM2.5', voc: 'VOC' }, bar_size: { small: 'Small', medium: 'Medium', large: 'Large', xlarge: 'Extra Large' }, bar_orientation: { ltr: 'Left to Right', rtl: 'Right to Left', up: 'Up (overlay)' }, bar_position: { default: 'Default', below: 'Bar below content', top: 'Bar on top (overlay)', bottom: 'Bar on bottom (overlay)', overlay: 'Bar overlay on content', background: 'Full card background' }, layout: { horizontal: 'Horizontal (default)', vertical: 'Vertical' } } } }, 'es-419': { card: { msg: { appliedDefaultValue: 'Se aplicó automáticamente el valor predeterminado.', attributeNotFound: 'No se encontró el atributo en Home Assistant.', discontinuousRange: 'El rango definido no es continuo.', entityNotFound: 'No se encontró la entidad en Home Assistant.', invalidActionObject: 'Objeto de acción inválido o mal estructurado.', invalidCustomThemeArray: 'El tema personalizado debe ser un arreglo.', invalidCustomThemeEntry: 'Uno o más elementos del tema personalizado son inválidos.', invalidDecimal: 'El valor debe ser un decimal válido.', invalidEntityId: 'ID de entidad inválido o mal formado.', invalidEnumValue: 'El valor proporcionado no está dentro de las opciones permitidas.', invalidIconType: 'El tipo de ícono especificado es inválido o desconocido.', invalidMaxValue: 'El valor máximo es inválido o excede el límite permitido.', invalidMinValue: 'El valor mínimo es inválido o está por debajo del límite permitido.', invalidStateContent: 'Contenido del estado inválido o mal formado.', invalidStateContentEntry: 'Uno o más elementos del contenido del estado son inválidos.', invalidTheme: 'El tema especificado es desconocido; se usará el tema predeterminado.', invalidTypeArray: 'Se esperaba un valor de tipo arreglo.', invalidTypeBoolean: 'Se esperaba un valor de tipo booleano.', invalidTypeNumber: 'Se esperaba un valor de tipo numérico.', invalidTypeObject: 'Se esperaba un valor de tipo objeto.', invalidTypeString: 'Se esperaba un valor de tipo cadena.', invalidUnionType: 'El valor no coincide con ningún tipo permitido.', minGreaterThanMax: 'El valor mínimo no puede ser mayor que el máximo.', missingActionKey: 'Falta una clave obligatoria en el objeto de acción.', missingColorProperty: 'Falta una propiedad de color obligatoria.', missingRequiredProperty: 'Falta una propiedad obligatoria.' } }, editor: { title: { content: 'Contenido', interaction: 'Interacción', theme: 'Apariencia y tema' }, field: { attribute: 'Atributo', badge_color: 'Color del distintivo', badge_icon: 'Ícono del distintivo', bar_color: 'Color de la barra de progreso', bar_effect: 'Efecto de la barra de progreso', bar_orientation: 'Orientación de la barra', bar_position: 'Posición de la barra', bar_single_line: 'Información en línea (superpuesta)', bar_size: 'Tamaño de la barra', center_zero: 'Cero centrado', color: 'Color principal', decimal: 'Decimal', disable_unit: 'Mostrar unidad', double_tap_action: 'Acción al doble toque', entity: 'Entidad', force_circular_background: 'Forzar fondo circular', hide: 'Ocultar', hold_action: 'Acción al mantener presionado', icon: 'Ícono', icon_double_tap_action: 'Acción de doble toque en ícono', icon_hold_action: 'Acción al mantener presionado ícono', icon_tap_action: 'Acción al tocar ícono', layout: 'Disposición del contenido', max_value: 'Valor máximo', max_value_attribute: 'Atributo (valor máximo)', max_value_entity: 'Usar valor máximo de la entidad', min_value: 'Valor mínimo', name: 'Nombre', percent: 'Porcentaje', reverse_secondary_info_row: 'Intercambiar barra y texto', secondary: 'Información secundaria', state_content: 'Contenido del estado', tap_action: 'Acción al tocar', text_shadow: 'Agregar sombra al texto (overlay)', theme: 'Tema', toggle_icon: 'Mostrar ícono', toggle_name: 'Mostrar nombre', toggle_progress_bar: 'Mostrar barra de progreso', toggle_secondary_info: 'Mostrar información secundaria', toggle_value: 'Mostrar valor', unit: 'Unidad', use_max_entity: 'Usar entidad como valor máximo' }, option: { theme: { optimal_when_low: 'Óptimo cuando es bajo (CPU, RAM…)', optimal_when_high: 'Óptimo cuando es alto (batería…)', light: 'Brillo', temperature: 'Temperatura', humidity: 'Humedad', pm25: 'PM2.5', voc: 'VOC' }, bar_size: { small: 'Pequeña', medium: 'Mediana', large: 'Grande', xlarge: 'Extra grande' }, bar_orientation: { ltr: 'De izquierda a derecha', rtl: 'De derecha a izquierda', up: 'Hacia arriba (superpuesta)' }, bar_position: { default: 'Predeterminado', below: 'Barra debajo del contenido', top: 'Barra arriba (superpuesta)', bottom: 'Barra abajo (superpuesta)', overlay: 'Superpuesta sobre el contenido', background: 'Fondo de la tarjeta' }, layout: { horizontal: 'Horizontal (predeterminado)', vertical: 'Vertical' } } } }, es: { card: { msg: { appliedDefaultValue: 'Se ha aplicado un valor predeterminado automáticamente.', attributeNotFound: 'Atributo no encontrado en Home Assistant.', discontinuousRange: 'El rango definido es discontinuo.', entityNotFound: 'Entidad no encontrada en Home Assistant.', invalidActionObject: 'El objeto de acción es inválido o está mal estructurado.', invalidCustomThemeArray: 'El tema personalizado debe ser un arreglo.', invalidCustomThemeEntry: 'Una o más entradas en el tema personalizado son inválidas.', invalidDecimal: 'El valor debe ser un número decimal válido.', invalidEntityId: 'El ID de la entidad no es válido o está mal formado.', invalidEnumValue: 'El valor proporcionado no es una opción válida.', invalidIconType: 'El tipo de icono especificado es inválido o no reconocido.', invalidMaxValue: 'El valor máximo es inválido o excede el límite permitido.', invalidMinValue: 'El valor mínimo es inválido o está por debajo del límite permitido.', invalidStateContent: 'El contenido del estado es inválido o está mal formado.', invalidStateContentEntry: 'Una o más entradas en el contenido del estado son inválidas.', invalidTheme: 'El tema especificado es desconocido. Se usará el tema por defecto.', invalidTypeArray: 'Se esperaba un valor de tipo arreglo.', invalidTypeBoolean: 'Se esperaba un valor de tipo booleano.', invalidTypeNumber: 'Se esperaba un valor de tipo número.', invalidTypeObject: 'Se esperaba un valor de tipo objeto.', invalidTypeString: 'Se esperaba un valor de tipo cadena.', invalidUnionType: 'El valor no coincide con ninguno de los tipos permitidos.', minGreaterThanMax: 'El valor mínimo no puede ser mayor que el valor máximo.', missingActionKey: 'Falta una clave obligatoria en el objeto de acción.', missingColorProperty: 'Falta una propiedad de color obligatoria.', missingRequiredProperty: 'Falta una propiedad obligatoria.' } }, editor: { title: { content: 'Contenido', interaction: 'Interacciones', theme: 'Apariencia y funcionamiento' }, field: { attribute: 'Atributo', badge_color: 'Color del badge', badge_icon: 'Icono del badge', bar_color: 'Color principal', bar_effect: 'Efecto de la barra', bar_orientation: 'Orientación de la barra', bar_position: 'Posición de la barra', bar_single_line: 'Información en una sola línea (overlay)', bar_size: 'Tamaño de la barra', center_zero: 'Cero en el centro', color: 'Color principal', decimal: 'decimal', disable_unit: 'Mostrar unidad', double_tap_action: 'Acción al pulsar dos veces', entity: 'Entidad', force_circular_background: 'Forzar fondo circular', hide: 'Ocultar', hold_action: 'Acción al mantener pulsado', icon: 'Icono', icon_double_tap_action: 'Acción al pulsar dos veces el icono', icon_hold_action: 'Acción al mantener pulsado el icono', icon_tap_action: 'Acción al pulsar el icono', layout: 'Disposición del contenido', max_value: 'Valor máximo', max_value_attribute: 'Atributo (max_value)', max_value_entity: 'Usar entidad como valor máximo', min_value: 'Valor mínimo', name: 'Nombre', percent: 'Porcentaje', reverse_secondary_info_row: 'Intercambiar barra y texto', secondary: 'Información secundaria', state_content: 'Contenido del estado', tap_action: 'Acción al pulsar brevemente', text_shadow: 'Añadir sombra al texto (overlay)', theme: 'Tema', toggle_icon: 'Mostrar icono', toggle_name: 'Mostrar nombre', toggle_progress_bar: 'Mostrar barra de progreso', toggle_secondary_info: 'Mostrar información secundaria', toggle_value: 'Mostrar valor', unit: 'Unidad', use_max_entity: 'Usar una entidad para el valor máximo' }, option: { theme: { optimal_when_low: 'Óptimo cuando es bajo (CPU, RAM,...)', optimal_when_high: 'Óptimo cuando es alto (Batería...)', light: 'Luz', temperature: 'Temperatura', humidity: 'Humedad', pm25: 'PM2.5', voc: 'VOC' }, bar_size: { small: 'Pequeña', medium: 'Mediana', large: 'Grande', xlarge: 'Extra grande' }, bar_orientation: { ltr: 'De izquierda a derecha', rtl: 'De derecha a izquierda', up: 'Hacia arriba (overlay)' }, bar_position: { default: 'Predeterminado', below: 'Barra debajo del contenido', top: 'Barra arriba (superpuesta)', bottom: 'Barra abajo (superpuesta)', overlay: 'Barra superpuesta al contenido (overlay)', background: 'Fondo de la tarjeta' }, layout: { horizontal: 'Horizontal (predeterminado)', vertical: 'Vertical' } } } }, et: { card: { msg: { appliedDefaultValue: 'Vaikimisi väärtus rakendati automaatselt.', attributeNotFound: 'Atribuut ei leitud Home Assistantis.', discontinuousRange: 'Määratud vahemik ei ole katkematu.', entityNotFound: 'Objekti ei leitud Home Assistantis.', invalidActionObject: 'Tegevuse objekt on vigane või valesti struktureeritud.', invalidCustomThemeArray: 'Kohandatud teema peab olema massiiv.', invalidCustomThemeEntry: 'Üks või mitu kohandatud teema kirjet on vigased.', invalidDecimal: 'Väärtus peab olema positiivne täisarv.', invalidEntityId: 'Objekti ID on vigane või valesti vormistatud.', invalidEnumValue: 'Antud väärtus ei kuulu lubatud valikute hulka.', invalidIconType: 'Määratud ikooni tüüp on vigane või tundmatu.', invalidMaxValue: 'Maksimaalne väärtus on vigane või ületab lubatud piiri.', invalidMinValue: 'Minimaalne väärtus on vigane või alla lubatud piiri.', invalidStateContent: 'Seisundi sisu on vigane või valesti vormistatud.', invalidStateContentEntry: 'Üks või mitu seisundi sisu kirjet on vigased.', invalidTheme: 'Määratud teema on tundmatu. Kasutatakse vaikimisi teemat.', invalidTypeArray: 'Oodati massiivi tüüpi väärtust.', invalidTypeBoolean: 'Oodati loogilist (boolean) tüüpi väärtust.', invalidTypeNumber: 'Oodati numbri tüüpi väärtust.', invalidTypeObject: 'Oodati objekti tüüpi väärtust.', invalidTypeString: 'Oodati stringi tüüpi väärtust.', invalidUnionType: 'Väärtus ei vasta ühelegi lubatud tüübile.', minGreaterThanMax: 'Minimaalne väärtus ei saa olla suurem kui maksimaalne.', missingActionKey: 'Tegevuse objektist puudub kohustuslik võti.', missingColorProperty: 'Puudub kohustuslik värvi atribuut.', missingRequiredProperty: 'Puudub kohustuslik atribuut.' } }, editor: { title: { content: 'Sisu', interaction: 'Interaktsioonid', theme: 'Välimus ja kasutatavus' }, field: { attribute: 'Atribuut', badge_color: 'Märgi värv', badge_icon: 'Märgi ikoon', bar_color: 'Riba värv', bar_effect: 'Riba efekt', bar_orientation: 'Riba orientatsioon', bar_position: 'Riba positsioon', bar_single_line: 'Info ühel real (overlay)', bar_size: 'Riba suurus', center_zero: 'Null keskel', color: 'Ikooni värv', decimal: 'Kümnendkoht', disable_unit: 'Näita ühikut', double_tap_action: 'Topeltpuudutuse tegevus', entity: 'Objekt', force_circular_background: 'Sunnitud ümmargune taust', hide: 'Peida', hold_action: 'Pikema vajutuse tegevus', icon: 'Ikoon', icon_double_tap_action: 'Ikooni topeltpuudutuse tegevus', icon_hold_action: 'Ikooni pika vajutuse tegevus', icon_tap_action: 'Ikooni puudutuse tegevus', layout: 'Sisu paigutus', max_value: 'Maksimaalne väärtus', max_value_attribute: 'Atribuut (max_value)', max_value_entity: 'Maksimaalne väärtus', min_value: 'Minimaalne väärtus', name: 'Nimi', percent: 'Protsent', reverse_secondary_info_row: 'Vaheta riba ja tekst', secondary: 'Täiendav info', state_content: 'Oleku sisu', tap_action: 'Puudutuse tegevus', text_shadow: 'Lisa teksti vari (overlay)', theme: 'Teema', toggle_icon: 'Näita ikooni', toggle_name: 'Näita nime', toggle_progress_bar: 'Näita edenemisriba', toggle_secondary_info: 'Näita täiendavat infot', toggle_value: 'Näita väärtust', unit: 'Ühik', use_max_entity: 'Kasuta objekti maksimaalse väärtuse jaoks' }, option: { theme: { optimal_when_low: 'Optimaalne madalatel väärtustel (CPU, RAM...)', optimal_when_high: 'Optimaalne kõrgetel väärtustel (Aku...)', light: 'Hele', temperature: 'Temperatuur', humidity: 'Niiskus', pm25: 'PM2.5', voc: 'VOC' }, bar_size: { small: 'Väike', medium: 'Keskmine', large: 'Suur', xlarge: 'Väga suur' }, bar_orientation: { ltr: 'Vasakult paremale', rtl: 'Paremalt vasakule', up: 'Üles (overlay)' }, bar_position: { default: 'Vaikimisi', below: 'Riba sisu all', top: 'Riba üleval (overlay)', bottom: 'Riba all (overlay)', overlay: 'Riba sisu kohal (overlay)', background: 'Kaardi taust' }, layout: { horizontal: 'Horisontaalne (vaikimisi)', vertical: 'Vertikaalne' } } } }, fi: { card: { msg: { appliedDefaultValue: 'Oletusarvo on asetettu automaattisesti.', attributeNotFound: 'Attribuuttia ei löytynyt Home Assistantista.', discontinuousRange: 'Määritetty alue on katkonainen.', entityNotFound: 'Entiteettiä ei löytynyt Home Assistantista.', invalidActionObject: 'Toiminto-objekti on virheellinen tai huonosti rakennettu.', invalidCustomThemeArray: 'Mukautetun teeman on oltava taulukko.', invalidCustomThemeEntry: 'Yksi tai useampi mukautetun teeman merkintä on virheellinen.', invalidDecimal: 'Arvon on oltava kelvollinen desimaaliluku.', invalidEntityId: 'Entiteetin tunniste on virheellinen tai väärin muotoiltu.', invalidEnumValue: 'Annettu arvo ei ole sallituista vaihtoehdoista.', invalidIconType: 'Annettu kuvaketyyppi on virheellinen tai tuntematon.', invalidMaxValue: 'Enimmäisarvo on virheellinen tai liian suuri.', invalidMinValue: 'Vähimmäisarvo on virheellinen tai liian pieni.', invalidStateContent: 'Tilasisältö on virheellinen tai väärässä muodossa.', invalidStateContentEntry: 'Yksi tai useampi tilasisällön merkintä on virheellinen.', invalidTheme: 'Määritetty teema on tuntematon. Käytetään oletusteemaa.', invalidTypeArray: 'Odotettiin taulukkoarvoa.', invalidTypeBoolean: 'Odotettiin totuusarvoa (boolean).', invalidTypeNumber: 'Odotettiin numeerista arvoa.', invalidTypeObject: 'Odotettiin objektityyppistä arvoa.', invalidTypeString: 'Odotettiin merkkijonotyyppistä arvoa.', invalidUnionType: 'Arvo ei vastaa mitään sallituista tyypeistä.', minGreaterThanMax: 'Vähimmäisarvo ei voi olla suurempi kuin enimmäisarvo.', missingActionKey: 'Toiminto-objektista puuttuu vaadittu avain.', missingColorProperty: 'Pakollinen väriominaisuus puuttuu.', missingRequiredProperty: 'Pakollinen ominaisuus puuttuu.' } }, editor: { title: { content: 'Sisältö', interaction: 'Vuorovaikutukset', theme: 'Ulkoasu' }, field: { attribute: 'Attribuutti', badge_color: 'Badge-väri', badge_icon: 'Badge-ikoni', bar_color: 'Pääväri', bar_effect: 'Palkin efekti', bar_orientation: 'Palkin suunta', bar_position: 'Palkin sijainti', bar_single_line: 'Tiedot yhdellä rivillä (overlay)', bar_size: 'Palkin koko', center_zero: 'Nolla keskellä', color: 'Pääväri', decimal: 'desimaali', disable_unit: 'Näytä yksikkö', double_tap_action: 'Toiminto kahdella napautuksella', entity: 'Entiteetti', force_circular_background: 'Pakota pyöreä tausta', hide: 'Piilota', hold_action: 'Toiminto pitkällä painalluksella', icon: 'Ikoni', icon_double_tap_action: 'Toiminto kahdella napautuksella kuvaketta', icon_hold_action: 'Toiminto pitkällä painalluksella kuvaketta', icon_tap_action: 'Toiminto kuvaketta napautettaessa', layout: 'Sisällön asettelu', max_value: 'Maksimiarvo', max_value_attribute: 'Attribuutti (max_value)', max_value_entity: 'Käytä entiteettiä maksimiarvona', min_value: 'Minimiarvo', name: 'Nimi', percent: 'Prosentti', reverse_secondary_info_row: 'Vaihda palkki ja teksti', secondary: 'Lisätiedot', state_content: 'Tilan sisältö', tap_action: 'Toiminto lyhyellä napautuksella', text_shadow: 'Lisää tekstivarjo (overlay)', theme: 'Teema', toggle_icon: 'Näytä ikoni', toggle_name: 'Näytä nimi', toggle_progress_bar: 'Näytä palkki', toggle_secondary_info: 'Näytä lisätiedot', toggle_value: 'Näytä arvo', unit: 'Yksikkö', use_max_entity: 'Käytä entiteettiä maksimiarvona' }, option: { theme: { optimal_when_low: 'Optimaalinen alhaisena (CPU, RAM...)', optimal_when_high: 'Optimaalinen korkeana (Akku...)', light: 'Valoisuus', temperature: 'Lämpötila', humidity: 'Kosteus', pm25: 'PM2.5', voc: 'VOC' }, bar_size: { small: 'Pieni', medium: 'Keski', large: 'Suuri', xlarge: 'Erittäin suuri' }, bar_orientation: { ltr: 'Vasemmalta oikealle', rtl: 'Oikealta vasemmalle', up: 'Ylös (overlay)' }, bar_position: { default: 'Oletus', below: 'Palkki sisällön alla', top: 'Palkki ylhäällä (päällekkäin)', bottom: 'Palkki alhaalla (päällekkäin)', overlay: 'Palkki sisällön päällä (overlay)', background: 'Kortin tausta' }, layout: { horizontal: 'Vaakasuora (oletus)', vertical: 'Pystysuora' } } } }, fr: { card: { msg: { appliedDefaultValue: 'Une valeur par défaut a été appliquée automatiquement.', attributeNotFound: 'Attribut introuvable dans Home Assistant.', discontinuousRange: 'L’intervalle défini est discontinu.', entityNotFound: 'Entité introuvable dans Home Assistant.', invalidActionObject: 'L’objet action est invalide ou mal structuré.', invalidCustomThemeArray: 'Le thème personnalisé doit être un tableau.', invalidCustomThemeEntry: 'Une ou plusieurs entrées du thème personnalisé sont invalides.', invalidDecimal: 'La valeur doit être un nombre entier positif.', invalidEntityId: 'L’identifiant de l’entité est invalide ou mal formé.', invalidEnumValue: 'La valeur fournie ne fait pas partie des options autorisées.', invalidIconType: 'Le type d’icône spécifié est invalide ou non reconnu.', invalidMaxValue: 'La valeur maximale est invalide ou au-dessus des limites autorisées.', invalidMinValue: 'La valeur minimale est invalide ou en dessous des limites autorisées.', invalidStateContent: 'Le contenu d’état est invalide ou mal formé.', invalidStateContentEntry: 'Une ou plusieurs entrées du contenu d’état sont invalides.', invalidTheme: 'Le thème spécifié est inconnu. Le thème par défaut sera utilisé.', invalidTypeArray: 'Une valeur de type tableau était attendue.', invalidTypeBoolean: 'Une valeur de type booléen était attendue.', invalidTypeNumber: 'Une valeur de type nombre était attendue.', invalidTypeObject: 'Une valeur de type objet était attendue.', invalidTypeString: 'Une valeur de type chaîne de caractères était attendue.', invalidUnionType: 'La valeur ne correspond à aucun des types autorisés.', minGreaterThanMax: 'La valeur minimale ne peut pas être supérieure à la valeur maximale.', missingActionKey: 'Une clé requise est manquante dans l’objet action.', missingColorProperty: 'Une propriété de couleur requise est manquante.', missingRequiredProperty: 'Une propriété requise est manquante.' } }, editor: { title: { content: 'Contenu', interaction: 'Interactions', theme: 'Aspect visuel et convivialité' }, field: { attribute: 'Attribut', badge_color: 'Couleur du badge', badge_icon: 'Icône du badge', bar_color: 'Couleur de la barre', bar_effect: 'Effet sur la barre', bar_orientation: 'Orientation de la barre', bar_position: 'Position de la barre', bar_single_line: 'Infos sur une ligne (overlay)', bar_size: 'Taille de la barre', center_zero: 'Zéro au centre', color: 'Couleur de l\'icône', decimal: 'décimal', disable_unit: 'Afficher l\'unité', double_tap_action: 'Comportement lors d\'un double appui', entity: 'Entité', force_circular_background: 'Forcer le fond circulaire', hide: 'Masquer', hold_action: 'Comportement lors d\'un appui long', icon: 'Icône', icon_double_tap_action: 'Comportement lors d\'un double appui sur l\'icône', icon_hold_action: 'Comportement lors d\'un appui long sur l\'icône', icon_tap_action: 'Comportement lors de l\'appui sur l\'icône', layout: 'Disposition du contenu', max_value: 'Valeur maximum', max_value_attribute: 'Attribut (max_value)', max_value_entity: 'Valeur maximum', min_value: 'Valeur minimum', name: 'Nom', percent: 'Pourcentage', reverse_secondary_info_row: 'Intervertir barre et texte', secondary: 'Information secondaire', state_content: 'Contenu de l’état', tap_action: 'Comportement lors d\'un appui court', text_shadow: 'Ajouter une ombre au texte (overlay)', theme: 'Thème', toggle_icon: 'Afficher l\'icône', toggle_name: 'Afficher le nom', toggle_progress_bar: 'Afficher la barre de progression', toggle_secondary_info: 'Afficher les informations secondaires', toggle_value: 'Afficher la valeur', unit: 'Unité', use_max_entity: 'Utiliser une entité pour la valeur max' }, option: { theme: { optimal_when_low: 'Optimal quand c\'est bas (CPU, RAM,...)', optimal_when_high: 'Optimal quand c\'est élevé (Batterie...)', light: 'Lumière', temperature: 'Température', humidity: 'Humidité', pm25: 'PM2.5', voc: 'VOC' }, bar_size: { small: 'Petite', medium: 'Moyenne', large: 'Grande', xlarge: 'Très grande' }, bar_orientation: { ltr: 'Gauche à droite', rtl: 'Droite à gauche', up: 'Vers le haut (overlay)' }, bar_position: { default: 'Défaut', below: 'Barre en dessous du contenu', top: 'Barre en haut (superposée)', bottom: 'Barre en bas (superposée)', overlay: 'Barre superposée au contenu (overlay)', background: 'Arrière-plan de la carte' }, layout: { horizontal: 'Horizontal (par défaut)', vertical: 'Vertical' } } } }, hi: { card: { msg: { appliedDefaultValue: 'एक डिफ़ॉल्ट मान स्वचालित रूप से लागू किया गया है।', attributeNotFound: 'HA में एट्रिब्यूट नहीं मिला।', discontinuousRange: 'परिभाषित रेंज असतत है।', entityNotFound: 'HA में एंटिटी नहीं मिली।', invalidActionObject: 'एक्शन ऑब्जेक्ट अमान्य या गलत तरीके से संरचित है।', invalidCustomThemeArray: 'कस्टम थीम एक एरे होना चाहिए।', invalidCustomThemeEntry: 'कस्टम थीम में एक या अधिक प्रविष्टियां अमान्य हैं।', invalidDecimal: 'मान एक वैध दशमलव संख्या होना चाहिए।', invalidEntityId: 'एंटिटी आईडी अमान्य या गलत तरीके से बनाई गई है।', invalidEnumValue: 'प्रदान किया गया मान अनुमतित विकल्पों में से एक नहीं है।', invalidIconType: 'निर्दिष्ट आइकन प्रकार अमान्य या अपरिचित है।', invalidMaxValue: 'अधिकतम मान अमान्य है या अनुमतित सीमा से ऊपर है।', invalidMinValue: 'न्यूनतम मान अमान्य है या अनुमतित सीमा से नीचे है।', invalidStateContent: 'स्थिति सामग्री अमान्य या गलत तरीके से बनाई गई है।', invalidStateContentEntry: 'स्थिति सामग्री में एक या अधिक प्रविष्टियां अमान्य हैं।', invalidTheme: 'निर्दिष्ट थीम अज्ञात है। डिफ़ॉल्ट थीम का उपयोग किया जाएगा।', invalidTypeArray: 'एरे प्रकार का मान अपेक्षित है।', invalidTypeBoolean: 'बूलियन प्रकार का मान अपेक्षित है।', invalidTypeNumber: 'संख्या प्रकार का मान अपेक्षित है।', invalidTypeObject: 'ऑब्जेक्ट प्रकार का मान अपेक्षित है।', invalidTypeString: 'स्ट्रिंग प्रकार का मान अपेक्षित है।', invalidUnionType: 'मान अनुमतित प्रकारों में से किसी से मेल नहीं खाता।', minGreaterThanMax: 'न्यूनतम मान अधिकतम मान से अधिक नहीं हो सकता।', missingActionKey: 'एक्शन ऑब्जेक्ट में एक आवश्यक कुंजी गायब है।', missingColorProperty: 'एक आवश्यक रंग गुण गायब है।', missingRequiredProperty: 'आवश्यक गुण गायब है।' } }, editor: { title: { content: 'सामग्री', interaction: 'बातचीत', theme: 'रूप और अनुभव' }, field: { attribute: 'एट्रिब्यूट', badge_color: 'बैज का रंग', badge_icon: 'बैज का आइकन', bar_color: 'मुख्य रंग', bar_effect: 'बार पर प्रभाव', bar_orientation: 'बार की दिशा', bar_position: 'बार की स्थिति', bar_single_line: 'एक पंक्ति में जानकारी (ओवरले)', bar_size: 'बार का आकार', center_zero: 'शून्य केंद्र में', color: 'मुख्य रंग', decimal: 'दशमलव', disable_unit: 'इकाई दिखाएँ', double_tap_action: 'डबल टैप व्यवहार', entity: 'एंटिटी', force_circular_background: 'गोलाकार पृष्ठभूमि को बाध्य करें', hide: 'छिपाएँ', hold_action: 'होल्ड व्यवहार', icon: 'आइकन', icon_double_tap_action: 'आइकन डबल टैप व्यवहार', icon_hold_action: 'आइकन होल्ड व्यवहार', icon_tap_action: 'आइकन टैप व्यवहार', layout: 'सामग्री लेआउट', max_value: 'अधिकतम मान', max_value_attribute: 'एट्रिब्यूट (max_value)', max_value_entity: 'एंटिटी का अधिकतम मान', min_value: 'न्यूनतम मान', name: 'नाम', percent: 'प्रतिशत', reverse_secondary_info_row: 'बार और टेक्स्ट बदलें', secondary: 'सहायक जानकारी', state_content: 'स्थिति की सामग्री', tap_action: 'टैप व्यवहार', text_shadow: 'टेक्स्ट में छाया जोड़ें (overlay)', theme: 'थीम', toggle_icon: 'आइकन दिखाएँ', toggle_name: 'नाम दिखाएँ', toggle_progress_bar: 'प्रगति बार दिखाएँ', toggle_secondary_info: 'सहायक जानकारी दिखाएँ', toggle_value: 'मान दिखाएँ', unit: 'इकाई', use_max_entity: 'अधिकतम मान के लिए एंटिटी उपयोग करें' }, option: { bar_orientation: { ltr: 'बाएं से दाएं', rtl: 'दाएं से बाएं', up: 'ऊपर की ओर (ओवरले)' }, bar_position: { below: 'सामग्री के नीचे बार', bottom: 'नीचे बार (ओवरले)', default: 'डिफ़ॉल्ट', overlay: 'सामग्री पर ओवरले बार', top: 'ऊपर बार (ओवरले)', background: 'कार्ड पृष्ठभूमि' }, bar_size: { large: 'बड़ी', medium: 'मध्यम', small: 'छोटी', xlarge: 'अतिरिक्त बड़ी' }, layout: { horizontal: 'क्षैतिज (डिफ़ॉल्ट)', vertical: 'लंबवत' }, theme: { humidity: 'आर्द्रता', light: 'प्रकाश', optimal_when_high: 'उच्च होने पर इष्टतम (बैटरी...)', optimal_when_low: 'कम होने पर इष्टतम (CPU, RAM,...)', pm25: 'PM2.5', temperature: 'तापमान', voc: 'VOC' } } } }, hr: { card: { msg: { appliedDefaultValue: 'Zadana vrijednost automatski je primijenjena.', attributeNotFound: 'Atribut nije pronađen u Home Assistantu.', discontinuousRange: 'Definirani raspon nije kontinuiran.', entityNotFound: 'Entitet nije pronađen u Home Assistantu.', invalidActionObject: 'Objekt radnje je nevažeći ili loše strukturiran.', invalidCustomThemeArray: 'Prilagođena tema mora biti polje.', invalidCustomThemeEntry: 'Jedan ili više unosa u temi su nevažeći.', invalidDecimal: 'Vrijednost mora biti valjani decimalni broj.', invalidEntityId: 'ID entiteta je nevažeći ili pogrešno formatiran.', invalidEnumValue: 'Navedena vrijednost nije među dopuštenim opcijama.', invalidIconType: 'Naveden tip ikone je nevažeći ili neprepoznatljiv.', invalidMaxValue: 'Maksimalna vrijednost je nevažeća ili previsoka.', invalidMinValue: 'Minimalna vrijednost je nevažeća ili preniska.', invalidStateContent: 'Sadržaj stanja je nevažeći ili pogrešno formatiran.', invalidStateContentEntry: 'Jedan ili više unosa stanja su nevažeći.', invalidTheme: 'Navedena tema je nepoznata. Koristi se zadana tema.', invalidTypeArray: 'Očekivana je vrijednost tipa polje.', invalidTypeBoolean: 'Očekivana je vrijednost tipa boolean.', invalidTypeNumber: 'Očekivana je vrijednost tipa broj.', invalidTypeObject: 'Očekivana je vrijednost tipa objekt.', invalidTypeString: 'Očekivana je vrijednost tipa string.', invalidUnionType: 'Vrijednost ne odgovara nijednom dopuštenom tipu.', minGreaterThanMax: 'Minimalna vrijednost ne može biti veća od maksimalne.', missingActionKey: 'Nedostaje obavezni ključ u objektu radnje.', missingColorProperty: 'Nedostaje obavezno svojstvo boje.', missingRequiredProperty: 'Nedostaje obavezno svojstvo.' } }, editor: { title: { content: 'Sadržaj', interaction: 'Interakcije', theme: 'Izgled i funkcionalnost' }, field: { attribute: 'Atribut', badge_color: 'Boja oznake', badge_icon: 'Ikona oznake', bar_color: 'Boja za traku', bar_effect: 'Efekt na traci', bar_orientation: 'Orijentacija trake', bar_position: 'Položaj trake', bar_single_line: 'Informacije u jednom retku (overlay)', bar_size: 'Veličina trake', center_zero: 'Nula u sredini', color: 'Primarna boja', decimal: 'decimalni', disable_unit: 'Prikaži jedinicu', double_tap_action: 'Radnja na dupli dodir', entity: 'Entitet', force_circular_background: 'Prisili kružnu pozadinu', hide: 'Sakrij', hold_action: 'Radnja na dugi dodir', icon: 'Ikona', icon_double_tap_action: 'Radnja na dupli dodir ikone', icon_hold_action: 'Radnja na dugi dodir ikone', icon_tap_action: 'Radnja na dodir ikone', layout: 'Raspored sadržaja', max_value: 'Maksimalna vrijednost', max_value_attribute: 'Atribut (max_value)', max_value_entity: 'Maksimalna vrijednost entiteta', min_value: 'Minimalna vrijednost', name: 'Ime', percent: 'Postotak', reverse_secondary_info_row: 'Zamijeni traku i tekst', secondary: 'Sekundarne informacije', state_content: 'Sadržaj stanja', tap_action: 'Radnja na kratki dodir', text_shadow: 'Dodaj sjenu tekstu (overlay)', theme: 'Tema', toggle_icon: 'Prikaži ikonu', toggle_name: 'Prikaži ime', toggle_progress_bar: 'Prikaži traku napretka', toggle_secondary_info: 'Prikaži sekundarne informacije', toggle_value: 'Prikaži vrijednost', unit: 'Jedinica', use_max_entity: 'Koristi entitet za maksimalnu vrijednost' }, option: { bar_orientation: { ltr: 'Lijevo na desno', rtl: 'Desno na lijevo', up: 'Prema gore (overlay)' }, bar_position: { below: 'Traka ispod sadržaja', bottom: 'Traka na dnu (overlay)', default: 'Zadano', overlay: 'Traka preklopljena na sadržaj (overlay)', top: 'Traka na vrhu (overlay)', background: 'Pozadina kartice' }, bar_size: { large: 'Velika', medium: 'Srednja', small: 'Mala', xlarge: 'Vrlo velika' }, layout: { horizontal: 'Horizontalno (zadano)', vertical: 'Vertikalno' }, theme: { humidity: 'Vlažnost', light: 'Svjetlo', optimal_when_high: 'Optimalno kada je visoko (Baterija...)', optimal_when_low: 'Optimalno kada je nisko (CPU, RAM,...)', pm25: 'PM2.5', temperature: 'Temperatura', voc: 'VOC' } } } }, hu: { card: { msg: { appliedDefaultValue: 'Alapértelmezett érték automatikusan alkalmazva.', attributeNotFound: 'Az attribútum nem található a Home Assistantban.', discontinuousRange: 'A megadott tartomány nem folytonos.', entityNotFound: 'Az entitás nem található a Home Assistantban.', invalidActionObject: 'Az action objektum érvénytelen vagy hibás felépítésű.', invalidCustomThemeArray: 'Az egyéni témának tömbnek kell lennie.', invalidCustomThemeEntry: 'Az egyéni téma egy vagy több bejegyzése érvénytelen.', invalidDecimal: 'Az értéknek pozitív egész számnak kell lennie.', invalidEntityId: 'Az entitás azonosító érvénytelen vagy hibás.', invalidEnumValue: 'A megadott érték nem része az engedélyezett opcióknak.', invalidIconType: 'A megadott ikon típus érvénytelen vagy nem ismert.', invalidMaxValue: 'A maximális érték érvénytelen vagy túl magas.', invalidMinValue: 'A minimális érték érvénytelen vagy túl alacsony.', invalidStateContent: 'Az állapot tartalma érvénytelen vagy hibás.', invalidStateContentEntry: 'Az állapot tartalmának egy vagy több eleme érvénytelen.', invalidTheme: 'Az adott téma ismeretlen. Az alapértelmezett téma lesz használva.', invalidTypeArray: 'Tömb típusú érték volt elvárva.', invalidTypeBoolean: 'Logikai (boolean) érték volt elvárva.', invalidTypeNumber: 'Szám típusú érték volt elvárva.', invalidTypeObject: 'Objektum típusú érték volt elvárva.', invalidTypeString: 'Szöveg típusú érték volt elvárva.', invalidUnionType: 'Az érték nem felel meg egyik engedélyezett típusnak sem.', minGreaterThanMax: 'A minimális érték nem lehet nagyobb a maximálisnál.', missingActionKey: 'Egy kötelező kulcs hiányzik az action objektumból.', missingColorProperty: 'Egy kötelező szín tulajdonság hiányzik.', missingRequiredProperty: 'Egy kötelező tulajdonság hiányzik.' } }, editor: { title: { content: 'Tartalom', interaction: 'Interakciók', theme: 'Megjelenés és használhatóság' }, field: { attribute: 'Attribútum', badge_color: 'Jelvény színe', badge_icon: 'Jelvény ikon', bar_color: 'Sáv színe', bar_effect: 'Sáv effektus', bar_orientation: 'Sáv iránya', bar_position: 'Sáv pozíciója', bar_single_line: 'Egy soros információ (overlay)', bar_size: 'Sáv mérete', center_zero: 'Nulla középen', color: 'Ikon színe', decimal: 'Tizedes', disable_unit: 'Egység megjelenítése', double_tap_action: 'Kettős koppintás művelet', entity: 'Entitás', force_circular_background: 'Kör alakú háttér erőltetése', hide: 'Elrejtés', hold_action: 'Hosszan tartó nyomás művelet', icon: 'Ikon', icon_double_tap_action: 'Ikon dupla koppintás művelet', icon_hold_action: 'Ikon hosszan nyomás művelet', icon_tap_action: 'Ikon koppintás művelet', layout: 'Tartalom elrendezése', max_value: 'Maximális érték', max_value_attribute: 'Attribútum (max_value)', max_value_entity: 'Maximális érték', min_value: 'Minimális érték', name: 'Név', percent: 'Százalék', reverse_secondary_info_row: 'Cserélje fel a sávot és a szöveget', secondary: 'Másodlagos információ', state_content: 'Állapot tartalma', tap_action: 'Koppintás művelet', text_shadow: 'Szöveg árnyék hozzáadása (overlay)', theme: 'Téma', toggle_icon: 'Ikon megjelenítése', toggle_name: 'Név megjelenítése', toggle_progress_bar: 'Sáv megjelenítése', toggle_secondary_info: 'Másodlagos info megjelenítése', toggle_value: 'Érték megjelenítése', unit: 'Mértékegység', use_max_entity: 'Entitás használata max értékhez' }, option: { theme: { optimal_when_low: 'Optimális alacsony értéknél (CPU, RAM...)', optimal_when_high: 'Optimális magas értéknél (Akkumulátor...)', light: 'Világos', temperature: 'Hőmérséklet', humidity: 'Páratartalom', pm25: 'PM2.5', voc: 'VOC' }, bar_size: { small: 'Kicsi', medium: 'Közepes', large: 'Nagy', xlarge: 'Nagyon nagy' }, bar_orientation: { ltr: 'Balról jobbra', rtl: 'Jobbról balra', up: 'Felfelé (overlay)' }, bar_position: { default: 'Alapértelmezett', below: 'Sáv a tartalom alatt', top: 'Sáv fent (overlay)', bottom: 'Sáv lent (overlay)', overlay: 'Sáv a tartalmon (overlay)', background: 'Kártya háttér' }, layout: { horizontal: 'Vízszintes (alapértelmezett)', vertical: 'Függőleges' } } } }, id: { card: { msg: { appliedDefaultValue: 'Nilai default telah diterapkan secara otomatis.', attributeNotFound: 'Atribut tidak ditemukan di HA.', discontinuousRange: 'Range yang didefinisikan tidak kontinu.', entityNotFound: 'Entitas tidak ditemukan di HA.', invalidActionObject: 'Objek aksi tidak valid atau struktur salah.', invalidCustomThemeArray: 'Tema kustom harus berupa array.', invalidCustomThemeEntry: 'Satu atau lebih entri dalam tema kustom tidak valid.', invalidDecimal: 'Nilai harus berupa angka desimal yang valid.', invalidEntityId: 'ID entitas tidak valid atau salah format.', invalidEnumValue: 'Nilai yang diberikan bukan salah satu dari opsi yang diizinkan.', invalidIconType: 'Tipe ikon yang ditentukan tidak valid atau tidak dikenali.', invalidMaxValue: 'Nilai maksimum tidak valid atau di atas batas yang diizinkan.', invalidMinValue: 'Nilai minimum tidak valid atau di bawah batas yang diizinkan.', invalidStateContent: 'Konten state tidak valid atau salah format.', invalidStateContentEntry: 'Satu atau lebih entri dalam konten state tidak valid.', invalidTheme: 'Tema yang ditentukan tidak dikenal. Tema default akan digunakan.', invalidTypeArray: 'Mengharapkan nilai bertipe array.', invalidTypeBoolean: 'Mengharapkan nilai bertipe boolean.', invalidTypeNumber: 'Mengharapkan nilai bertipe angka.', invalidTypeObject: 'Mengharapkan nilai bertipe object.', invalidTypeString: 'Mengharapkan nilai bertipe string.', invalidUnionType: 'Nilai tidak cocok dengan tipe yang diizinkan.', minGreaterThanMax: 'Nilai minimum tidak boleh lebih besar dari nilai maksimum.', missingActionKey: 'Kunci yang diperlukan hilang dalam objek aksi.', missingColorProperty: 'Properti warna yang diperlukan hilang.', missingRequiredProperty: 'Properti yang diperlukan hilang.' } }, editor: { title: { content: 'Konten', interaction: 'Interaksi', theme: 'Tampilan & Nuansa' }, field: { attribute: 'Atribut', badge_color: 'Warna lencana', badge_icon: 'Ikon lencana', bar_color: 'Warna bar', bar_effect: 'Efek pada bar', bar_orientation: 'Orientasi bar', bar_position: 'Posisi bar', bar_single_line: 'Info dalam satu baris (overlay)', bar_size: 'Ukuran bar', center_zero: 'Nol di tengah', color: 'Warna utama', decimal: 'desimal', disable_unit: 'Tampilkan unit', double_tap_action: 'Perilaku ketuk ganda', entity: 'Entitas', force_circular_background: 'Paksa latar belakang melingkar', hide: 'Sembunyikan', hold_action: 'Perilaku tahan', icon: 'Ikon', icon_double_tap_action: 'Perilaku ketuk ganda ikon', icon_hold_action: 'Perilaku tahan ikon', icon_tap_action: 'Perilaku ketuk ikon', layout: 'Tata letak konten', max_value: 'Nilai maksimum', max_value_attribute: 'Atribut (max_value)', max_value_entity: 'Nilai maksimum', min_value: 'Nilai minimum', name: 'Nama', percent: 'Persentase', reverse_secondary_info_row: 'Tukar bilah dan teks', secondary: 'Informasi sekunder', state_content: 'Konten status', tap_action: 'Perilaku ketuk', text_shadow: 'Tambahkan bayangan teks (overlay)', theme: 'Tema', toggle_icon: 'Tampilkan ikon', toggle_name: 'Tampilkan nama', toggle_progress_bar: 'Tampilkan bar kemajuan', toggle_secondary_info: 'Tampilkan informasi sekunder', toggle_value: 'Tampilkan nilai', unit: 'Unit', use_max_entity: 'Gunakan entitas untuk nilai maksimum' }, option: { bar_orientation: { ltr: 'Kiri ke kanan', rtl: 'Kanan ke kiri', up: 'Ke atas (overlay)' }, bar_position: { below: 'Bar di bawah konten', bottom: 'Bar di bawah (overlay)', default: 'Default', overlay: 'Bar ditumpangkan pada konten (overlay)', top: 'Bar di atas (overlay)', background: 'Latar belakang kartu' }, bar_size: { large: 'Besar', medium: 'Sedang', small: 'Kecil', xlarge: 'Sangat besar' }, layout: { horizontal: 'Horizontal (default)', vertical: 'Vertikal' }, theme: { humidity: 'Kelembaban', light: 'Cahaya', optimal_when_high: 'Optimal saat Tinggi (Baterai...)', optimal_when_low: 'Optimal saat Rendah (CPU, RAM,...)', pm25: 'PM2.5', temperature: 'Suhu', voc: 'VOC' } } } }, it: { card: { msg: { appliedDefaultValue: 'È stato applicato automaticamente un valore predefinito.', attributeNotFound: 'Attributo non trovato in Home Assistant.', discontinuousRange: 'L\'intervallo definito è discontinuo.', entityNotFound: 'Entità non trovata in Home Assistant.', invalidActionObject: 'L\'oggetto azione non è valido o è strutturato in modo errato.', invalidCustomThemeArray: 'Il tema personalizzato deve essere un array.', invalidCustomThemeEntry: 'Una o più voci del tema personalizzato non sono valide.', invalidDecimal: 'Il valore deve essere un numero decimale valido.', invalidEntityId: 'L\'ID dell\'entità non è valido o è mal formattato.', invalidEnumValue: 'Il valore fornito non è tra quelli consentiti.', invalidIconType: 'Il tipo di icona specificato non è valido o non è riconosciuto.', invalidMaxValue: 'Il valore massimo non è valido o supera il limite consentito.', invalidMinValue: 'Il valore minimo non è valido o è al di sotto del limite consentito.', invalidStateContent: 'Il contenuto dello stato non è valido o è mal formattato.', invalidStateContentEntry: 'Una o più voci nel contenuto dello stato non sono valide.', invalidTheme: 'Il tema specificato è sconosciuto. Verrà utilizzato il tema predefinito.', invalidTypeArray: 'Atteso un valore di tipo array.', invalidTypeBoolean: 'Atteso un valore di tipo booleano.', invalidTypeNumber: 'Atteso un valore di tipo numero.', invalidTypeObject: 'Atteso un valore di tipo oggetto.', invalidTypeString: 'Atteso un valore di tipo stringa.', invalidUnionType: 'Il valore non corrisponde a nessuno dei tipi consentiti.', minGreaterThanMax: 'Il valore minimo non può essere superiore al valore massimo.', missingActionKey: 'Manca una chiave obbligatoria nell\'oggetto azione.', missingColorProperty: 'Manca una proprietà colore obbligatoria.', missingRequiredProperty: 'Proprietà obbligatoria mancante.' } }, editor: { title: { content: 'Contenuto', interaction: 'Interazioni', theme: 'Aspetto e funzionalità' }, field: { attribute: 'Attributo', badge_color: 'Colore del badge', badge_icon: 'Icona del badge', bar_color: 'Colore per la barra', bar_effect: 'Effetto sulla barra', bar_orientation: 'Orientamento della barra', bar_position: 'Posizione della barra', bar_single_line: 'Info su una riga (overlay)', bar_size: 'Dimensione della barra', center_zero: 'Zero al centro', color: 'Colore dell\'icona', decimal: 'Decimale', disable_unit: 'Mostra unità', double_tap_action: 'Azione al doppio tocco', entity: 'Entità', force_circular_background: 'Forza sfondo circolare', hide: 'Nascondi', hold_action: 'Azione al tocco prolungato', icon: 'Icona', icon_double_tap_action: 'Azione al doppio tocco dell\'icona', icon_hold_action: 'Azione al tocco prolungato dell\'icona', icon_tap_action: 'Azione al tocco dell\'icona', layout: 'Layout del contenuto', max_value: 'Valore massimo', max_value_attribute: 'Attributo (max_value)', max_value_entity: 'Valore massimo', min_value: 'Valore minimo', name: 'Nome', percent: 'Percentuale', reverse_secondary_info_row: 'Scambia barra e testo', secondary: 'Informazione secondaria', state_content: 'Contenuto dello stato', tap_action: 'Azione al tocco breve', text_shadow: 'Aggiungi ombra al testo (overlay)', theme: 'Tema', toggle_icon: 'Mostra icona', toggle_name: 'Mostra nome', toggle_progress_bar: 'Mostra barra di avanzamento', toggle_secondary_info: 'Mostra informazioni secondarie', toggle_value: 'Mostra valore', unit: 'Unità', use_max_entity: 'Usa un\'entità per il valore massimo' }, option: { bar_orientation: { ltr: 'Da sinistra a destra', rtl: 'Da destra a sinistra', up: 'Verso l\'alto (overlay)' }, bar_position: { below: 'Barra sotto il contenuto', bottom: 'Barra in basso (overlay)', default: 'Predefinito', overlay: 'Barra sovrapposta al contenuto (overlay)', top: 'Barra in alto (overlay)', background: 'Sfondo della scheda' }, bar_size: { large: 'Grande', medium: 'Media', small: 'Piccola', xlarge: 'Extra grande' }, layout: { horizontal: 'Orizzontale (predefinito)', vertical: 'Verticale' }, theme: { humidity: 'Umidità', light: 'Luce', optimal_when_high: 'Ottimale quando è alto (Batteria...)', optimal_when_low: 'Ottimale quando è basso (CPU, RAM,...)', pm25: 'PM2.5', temperature: 'Temperatura', voc: 'VOC' } } } }, ja: { card: { msg: { appliedDefaultValue: 'デフォルト値が自動的に適用されました。', attributeNotFound: 'Home Assistant に属性が見つかりません。', discontinuousRange: '定義された範囲が連続していません。', entityNotFound: 'Home Assistant にエンティティが見つかりません。', invalidActionObject: 'アクションオブジェクトが無効または構造が不正です。', invalidCustomThemeArray: 'カスタムテーマは配列である必要があります。', invalidCustomThemeEntry: 'カスタムテーマの1つ以上のエントリが無効です。', invalidDecimal: '値は有効な小数である必要があります。', invalidEntityId: 'エンティティ ID が無効か、形式が正しくありません。', invalidEnumValue: '指定された値は許可されたオプションのいずれでもありません。', invalidIconType: '指定されたアイコンタイプが無効または認識されません。', invalidMaxValue: '最大値が無効か、許容範囲を超えています。', invalidMinValue: '最小値が無効か、許容範囲を下回っています。', invalidStateContent: '状態の内容が無効または形式が不正です。', invalidStateContentEntry: '状態の内容の1つ以上のエントリが無効です。', invalidTheme: '指定されたテーマは不明です。デフォルトのテーマが使用されます。', invalidTypeArray: '配列型の値が必要です。', invalidTypeBoolean: 'ブール型の値が必要です。', invalidTypeNumber: '数値型の値が必要です。', invalidTypeObject: 'オブジェクト型の値が必要です。', invalidTypeString: '文字列型の値が必要です。', invalidUnionType: '値が許可された型のいずれにも一致しません。', minGreaterThanMax: '最小値は最大値より大きくできません。', missingActionKey: 'アクションオブジェクトに必要なキーが欠落しています。', missingColorProperty: '必要な色のプロパティが欠落しています。', missingRequiredProperty: '必要なプロパティが欠落しています。' } }, editor: { title: { content: 'コンテンツ', interaction: 'インタラクション', theme: '外観' }, field: { attribute: '属性', badge_color: 'バッジの色', badge_icon: 'バッジのアイコン', bar_color: 'バーの色', bar_effect: 'バーのエフェクト', bar_orientation: 'バーの向き', bar_position: 'バーの位置', bar_single_line: '1行で情報を表示(オーバーレイ)', bar_size: 'バーサイズ', center_zero: 'ゼロを中央に', color: 'メインカラー', decimal: '小数点', disable_unit: '単位を表示', double_tap_action: 'ダブルタップしたときの動作', entity: 'エンティティ', force_circular_background: '円形の背景を強制する', hide: '非表示', hold_action: '長押ししたときの動作', icon: 'アイコン', icon_double_tap_action: 'アイコンをダブルタップしたときの動作', icon_hold_action: 'アイコンを長押ししたときの動作', icon_tap_action: 'アイコンをタップしたときの動作', layout: 'コンテンツのレイアウト', max_value: '最大値', max_value_attribute: '属性(最大値)', max_value_entity: '最大値', min_value: '最小値', name: '名前', percent: 'パーセント', reverse_secondary_info_row: 'バーとテキストを入れ替える', secondary: '補足情報', state_content: '状態の内容', tap_action: '短くタップしたときの動作', text_shadow: 'テキストに影を追加 (オーバーレイ)', theme: 'テーマ', toggle_icon: 'アイコンを表示', toggle_name: '名前を表示', toggle_progress_bar: 'プログレスバーを表示', toggle_secondary_info: '補足情報を表示', toggle_value: '値を表示', unit: '単位', use_max_entity: '最大値にエンティティを使用' }, option: { bar_orientation: { ltr: '左から右', rtl: '右から左', up: '上方向(オーバーレイ)' }, bar_position: { below: 'コンテンツの下にバー', bottom: '下部にバー(オーバーレイ)', default: 'デフォルト', overlay: 'コンテンツに重ねてバー(オーバーレイ)', top: '上部にバー(オーバーレイ)', background: 'カードの背景' }, bar_size: { large: '大', medium: '中', small: '小', xlarge: '特大' }, layout: { horizontal: '水平(デフォルト)', vertical: '垂直' }, theme: { humidity: '湿度', light: '明るさ', optimal_when_high: '高い時が最適(バッテリーなど)', optimal_when_low: '低い時が最適(CPU、RAMなど)', pm25: 'PM2.5', temperature: '温度', voc: 'VOC' } } } }, ko: { card: { msg: { appliedDefaultValue: '기본값이 자동으로 적용되었습니다.', attributeNotFound: 'Home Assistant에서 속성을 찾을 수 없습니다.', discontinuousRange: '정의된 범위가 연속적이지 않습니다.', entityNotFound: 'Home Assistant에서 엔티티를 찾을 수 없습니다.', invalidActionObject: '액션 객체가 잘못되었거나 구조가 올바르지 않습니다.', invalidCustomThemeArray: '사용자 정의 테마는 배열이어야 합니다.', invalidCustomThemeEntry: '사용자 정의 테마에 하나 이상의 잘못된 항목이 있습니다.', invalidDecimal: '값은 유효한 소수여야 합니다.', invalidEntityId: '엔티티 ID가 잘못되었거나 형식이 잘못되었습니다.', invalidEnumValue: '제공된 값이 허용된 옵션 중 하나가 아닙니다.', invalidIconType: '지정된 아이콘 유형이 잘못되었거나 인식되지 않습니다.', invalidMaxValue: '최대값이 유효하지 않거나 허용된 범위를 초과합니다.', invalidMinValue: '최소값이 유효하지 않거나 허용된 범위보다 작습니다.', invalidStateContent: '상태 콘텐츠가 잘못되었거나 형식이 잘못되었습니다.', invalidStateContentEntry: '상태 콘텐츠에 하나 이상의 잘못된 항목이 있습니다.', invalidTheme: '지정된 테마를 알 수 없습니다. 기본 테마가 사용됩니다.', invalidTypeArray: '배열 유형의 값이 필요합니다.', invalidTypeBoolean: '불리언 유형의 값이 필요합니다.', invalidTypeNumber: '숫자 유형의 값이 필요합니다.', invalidTypeObject: '객체 유형의 값이 필요합니다.', invalidTypeString: '문자열 유형의 값이 필요합니다.', invalidUnionType: '값이 허용된 유형 중 어떤 것과도 일치하지 않습니다.', minGreaterThanMax: '최소값은 최대값보다 클 수 없습니다.', missingActionKey: '액션 객체에 필수 키가 없습니다.', missingColorProperty: '필수 색상 속성이 누락되었습니다.', missingRequiredProperty: '필수 속성이 누락되었습니다.' } }, editor: { title: { content: '콘텐츠', interaction: '상호작용', theme: '테마 및 스타일' }, field: { attribute: '속성', badge_color: '배지 색상', badge_icon: '배지 아이콘', bar_color: '바 색상', bar_effect: '바 효과', bar_orientation: '바 방향', bar_position: '바 위치', bar_single_line: '한 줄로 정보 표시 (오버레이)', bar_size: '바 크기', center_zero: '중앙에 영점', color: '기본 색상', decimal: '소수점', disable_unit: '단위 표시', double_tap_action: '더블 탭 시 동작', entity: '엔티티', force_circular_background: '원형 배경 강제 적용', hide: '숨기기', hold_action: '길게 누를 시 동작', icon: '아이콘', icon_double_tap_action: '아이콘 더블 탭 시 동작', icon_hold_action: '아이콘 길게 누를 시 동작', icon_tap_action: '아이콘 탭 시 동작', layout: '콘텐츠 레이아웃', max_value: '최대값', max_value_attribute: '속성 (최대값)', max_value_entity: '최대값', min_value: '최소값', name: '이름', percent: '퍼센트', reverse_secondary_info_row: '막대와 텍스트 교체', secondary: '보조 정보', state_content: '상태 콘텐츠', tap_action: '짧게 탭 시 동작', text_shadow: '텍스트 그림자 추가 (오버레이)', theme: '테마', toggle_icon: '아이콘 표시', toggle_name: '이름 표시', toggle_progress_bar: '진행 바 표시', toggle_secondary_info: '보조 정보 표시', toggle_value: '값 표시', unit: '단위', use_max_entity: '최대값에 엔티티 사용' }, option: { bar_orientation: { ltr: '왼쪽에서 오른쪽', rtl: '오른쪽에서 왼쪽', up: '위쪽 방향 (오버레이)' }, bar_position: { below: '콘텐츠 아래 바', bottom: '하단 바 (오버레이)', default: '기본', overlay: '콘텐츠 위에 바 (오버레이)', top: '상단 바 (오버레이)', background: '카드 배경' }, bar_size: { large: '큰', medium: '중간', small: '작은', xlarge: '매우 큰' }, layout: { horizontal: '수평 (기본)', vertical: '수직' }, theme: { humidity: '습도', light: '조도', optimal_when_high: '높을 때 최적 (배터리 등)', optimal_when_low: '낮을 때 최적 (CPU, RAM 등)', pm25: 'PM2.5', temperature: '온도', voc: 'VOC' } } } }, lt: { card: { msg: { appliedDefaultValue: 'Numatytoji reikšmė buvo automatiškai pritaikyta.', attributeNotFound: 'Atributas nerastas Home Assistant.', discontinuousRange: 'Nustatytas intervalas nėra tęstinis.', entityNotFound: 'Entity nerasta Home Assistant.', invalidActionObject: 'Veiksmo objektas negalioja arba neteisingai suformuotas.', invalidCustomThemeArray: 'Individuali tema turi būti masyvas.', invalidCustomThemeEntry: 'Viena ar daugiau individualios temos įrašų negalioja.', invalidDecimal: 'Reikšmė turi būti teigiamas sveikasis skaičius.', invalidEntityId: 'Entity ID negalioja arba neteisingas.', invalidEnumValue: 'Pateikta reikšmė nėra tarp leidžiamų parinkčių.', invalidIconType: 'Nurodytas piktogramos tipas negalioja arba nežinomas.', invalidMaxValue: 'Maksimali reikšmė negalioja arba viršija leistiną ribą.', invalidMinValue: 'Minimali reikšmė negalioja arba žemesnė už leistiną ribą.', invalidStateContent: 'Būsenos turinys negalioja arba neteisingai suformuotas.', invalidStateContentEntry: 'Viena ar daugiau būsenos turinio įrašų negalioja.', invalidTheme: 'Nurodyta tema nežinoma. Bus naudojama numatytoji tema.', invalidTypeArray: 'Tikėtasi masyvo tipo reikšmės.', invalidTypeBoolean: 'Tikėtasi loginės reikšmės (boolean).', invalidTypeNumber: 'Tikėtasi skaičiaus tipo reikšmės.', invalidTypeObject: 'Tikėtasi objekto tipo reikšmės.', invalidTypeString: 'Tikėtasi eilutės tipo reikšmės.', invalidUnionType: 'Reikšmė neatitinka nė vieno leidžiamo tipo.', minGreaterThanMax: 'Minimali reikšmė negali būti didesnė už maksimalų.', missingActionKey: 'Trūksta privalomo rakto veiksmo objekte.', missingColorProperty: 'Trūksta privalomos spalvos savybės.', missingRequiredProperty: 'Trūksta privalomos savybės.' } }, editor: { title: { content: 'Turinys', interaction: 'Sąveikos', theme: 'Išvaizda ir naudojamumas' }, field: { attribute: 'Atributas', badge_color: 'Ženklelio spalva', badge_icon: 'Ženklelio ikona', bar_color: 'Juostos spalva', bar_effect: 'Juostos efektas', bar_orientation: 'Juostos orientacija', bar_position: 'Juostos pozicija', bar_single_line: 'Informacija vienoje eilutėje (overlay)', bar_size: 'Juostos dydis', center_zero: 'Nulis centre', color: 'Ikonos spalva', decimal: 'Dešimtainė', disable_unit: 'Rodyti vienetą', double_tap_action: 'Dviejų bakstelėjimų veiksmas', entity: 'Entity', force_circular_background: 'Priversti apvalų foną', hide: 'Slėpti', hold_action: 'Ilgo paspaudimo veiksmas', icon: 'Ikona', icon_double_tap_action: 'Ikonos dviejų bakstelėjimų veiksmas', icon_hold_action: 'Ikonos ilgo paspaudimo veiksmas', icon_tap_action: 'Ikonos bakstelėjimo veiksmas', layout: 'Turinio išdėstymas', max_value: 'Maksimali reikšmė', max_value_attribute: 'Atributas (max_value)', max_value_entity: 'Maksimali reikšmė', min_value: 'Minimali reikšmė', name: 'Pavadinimas', percent: 'Procentai', reverse_secondary_info_row: 'Sukeisti juostą ir tekstą', secondary: 'Papildoma informacija', state_content: 'Būsenos turinys', tap_action: 'Bakstelėjimo veiksmas', text_shadow: 'Pridėti teksto šešėlį (overlay)', theme: 'Tema', toggle_icon: 'Rodyti ikoną', toggle_name: 'Rodyti pavadinimą', toggle_progress_bar: 'Rodyti progreso juostą', toggle_secondary_info: 'Rodyti papildomą info', toggle_value: 'Rodyti reikšmę', unit: 'Vienetas', use_max_entity: 'Naudoti entity max reikšmei' }, option: { theme: { optimal_when_low: 'Optimalu esant žemoms reikšmėms (CPU, RAM...)', optimal_when_high: 'Optimalu esant aukštoms reikšmėms (Baterija...)', light: 'Šviesi', temperature: 'Temperatūra', humidity: 'Drėgmė', pm25: 'PM2.5', voc: 'VOC' }, bar_size: { small: 'Maža', medium: 'Vidutinė', large: 'Didelė', xlarge: 'Labai didelė' }, bar_orientation: { ltr: 'Iš kairės į dešinę', rtl: 'Iš dešinės į kairę', up: 'Į viršų (overlay)' }, bar_position: { default: 'Numatyta', below: 'Juosta po turiniu', top: 'Juosta viršuje (overlay)', bottom: 'Juosta apačioje (overlay)', overlay: 'Juosta ant turinio (overlay)', background: 'Kortelės fonas' }, layout: { horizontal: 'Horizontalus (numatyta)', vertical: 'Vertikalus' } } } }, lv: { card: { msg: { appliedDefaultValue: 'Noklusējuma vērtība tika automātiski piemērota.', attributeNotFound: 'Atribūts nav atrasts Home Assistant.', discontinuousRange: 'Norādītais diapazons nav nepārtraukts.', entityNotFound: 'Vienība nav atrasta Home Assistant.', invalidActionObject: 'Darbības objekts nav derīgs vai ir nepareizi strukturēts.', invalidCustomThemeArray: 'Pielāgotajai tēmai jābūt masīvam.', invalidCustomThemeEntry: 'Viena vai vairākas pielāgotās tēmas ievades nav derīgas.', invalidDecimal: 'Vērtībai jābūt pozitīvam veselajam skaitlim.', invalidEntityId: 'Vienības ID nav derīgs vai ir nepareizs.', invalidEnumValue: 'Ievadītā vērtība nav atļauto opciju sarakstā.', invalidIconType: 'Norādītais ikonas tips nav derīgs vai nav zināms.', invalidMaxValue: 'Maksimālā vērtība nav derīga vai pārsniedz atļauto robežu.', invalidMinValue: 'Minimālā vērtība nav derīga vai zem atļautās robežas.', invalidStateContent: 'Stāvokļa saturs nav derīgs vai ir nepareizi strukturēts.', invalidStateContentEntry: 'Viena vai vairākas stāvokļa satura ievades nav derīgas.', invalidTheme: 'Norādītā tēma nav zināma. Tiks izmantota noklusējuma tēma.', invalidTypeArray: 'Tika gaidīta masīva tipa vērtība.', invalidTypeBoolean: 'Tika gaidīta loģiska (boolean) vērtība.', invalidTypeNumber: 'Tika gaidīta skaitļa tipa vērtība.', invalidTypeObject: 'Tika gaidīta objekta tipa vērtība.', invalidTypeString: 'Tika gaidīta virknes tipa vērtība.', invalidUnionType: 'Vērtība neatbilst nevienam atļautajam tipam.', minGreaterThanMax: 'Minimālā vērtība nevar būt lielāka par maksimālo.', missingActionKey: 'Trūkst obligātā atslēga darbības objektā.', missingColorProperty: 'Trūkst obligātā krāsas īpašība.', missingRequiredProperty: 'Trūkst obligātā īpašība.' } }, editor: { title: { content: 'Saturs', interaction: 'Saskarsme', theme: 'Izskats un lietojamība' }, field: { attribute: 'Atribūts', badge_color: 'Žetona krāsa', badge_icon: 'Žetona ikona', bar_color: 'Joslas krāsa', bar_effect: 'Joslas efekts', bar_orientation: 'Joslas orientācija', bar_position: 'Joslas pozīcija', bar_single_line: 'Informācija vienā rindā (overlay)', bar_size: 'Joslas izmērs', center_zero: 'Nulles centrā', color: 'Ikonas krāsa', decimal: 'Decimāldaļa', disable_unit: 'Rādīt vienību', double_tap_action: 'Divreiz pieskaroties', entity: 'Vienība', force_circular_background: 'Piespiest apļu fonu', hide: 'Slēpt', hold_action: 'Ilgs pieskāriens', icon: 'Ikona', icon_double_tap_action: 'Ikonas dubults pieskāriens', icon_hold_action: 'Ikonas ilgs pieskāriens', icon_tap_action: 'Ikonas pieskāriens', layout: 'Satura izkārtojums', max_value: 'Maksimālā vērtība', max_value_attribute: 'Atribūts (max_value)', max_value_entity: 'Maksimālā vērtība', min_value: 'Minimālā vērtība', name: 'Nosaukums', percent: 'Procenti', reverse_secondary_info_row: 'Mainīt joslu un tekstu', secondary: 'Papildu informācija', state_content: 'Stāvokļa saturs', tap_action: 'Pieskāriens', text_shadow: 'Pievienot teksta ēnu (overlay)', theme: 'Tēma', toggle_icon: 'Rādīt ikonu', toggle_name: 'Rādīt nosaukumu', toggle_progress_bar: 'Rādīt progresa joslu', toggle_secondary_info: 'Rādīt papildu info', toggle_value: 'Rādīt vērtību', unit: 'Vienība', use_max_entity: 'Izmantot vienību max vērtībai' }, option: { theme: { optimal_when_low: 'Optimāli pie zemām vērtībām (CPU, RAM...)', optimal_when_high: 'Optimāli pie augstām vērtībām (Akumulators...)', light: 'Gaiša', temperature: 'Temperatūra', humidity: 'Mitrums', pm25: 'PM2.5', voc: 'VOC' }, bar_size: { small: 'Mazs', medium: 'Vidējs', large: 'Liels', xlarge: 'Ļoti liels' }, bar_orientation: { ltr: 'No kreisās uz labo', rtl: 'No labās uz kreiso', up: 'Uz augšu (overlay)' }, bar_position: { default: 'Noklusētais', below: 'Josla zem satura', top: 'Josla augšā (overlay)', bottom: 'Josla apakšā (overlay)', overlay: 'Josla virs satura (overlay)', background: 'Kartes fons' }, layout: { horizontal: 'Horizontāls (noklusēts)', vertical: 'Vertikāls' } } } }, mk: { card: { msg: { appliedDefaultValue: 'Автоматски е применета стандардна вредност.', attributeNotFound: 'Атрибутот не е пронајден во Home Assistant.', discontinuousRange: 'Дефинираниот опсег е дисконинуиран.', entityNotFound: 'Ентитетот не е пронајден во Home Assistant.', invalidActionObject: 'Објектот за акција е невалиден или неправилно структуриран.', invalidCustomThemeArray: 'Прилагодената тема мора да биде низа.', invalidCustomThemeEntry: 'Еден или повеќе елементи во прилагодената тема се невалидни.', invalidDecimal: 'Вредноста мора да биде валиден децимален број.', invalidEntityId: 'ID-то на ентитетот е невалидно или лошо форматирано.', invalidEnumValue: 'Дадената вредност не е дозволена опција.', invalidIconType: 'Типот на икона е невалиден или непознат.', invalidMaxValue: 'Максималната вредност е невалидна или над дозволеното.', invalidMinValue: 'Минималната вредност е невалидна или под дозволеното.', invalidStateContent: 'Состојбата е невалидна или лошо форматирана.', invalidStateContentEntry: 'Еден или повеќе елементи во состојбата се невалидни.', invalidTheme: 'Темата е непозната. Ќе се користи стандардна тема.', invalidTypeArray: 'Се очекуваше вредност од тип низа.', invalidTypeBoolean: 'Се очекуваше вредност од тип boolean.', invalidTypeNumber: 'Се очекуваше вредност од тип број.', invalidTypeObject: 'Се очекуваше вредност од тип објект.', invalidTypeString: 'Се очекуваше вредност од тип string.', invalidUnionType: 'Вредноста не одговара на дозволените типови.', minGreaterThanMax: 'Минималната вредност не може да биде поголема од максималната.', missingActionKey: 'Недостасува потребен клуч во објектот за акција.', missingColorProperty: 'Недостасува потребна карактеристика за боја.', missingRequiredProperty: 'Недостасува потребно својство.' } }, editor: { title: { content: 'Содржина', interaction: 'Интеракции', theme: 'Изглед и функционалност' }, field: { attribute: 'Атрибут', badge_color: 'Боја на значка', badge_icon: 'Икона на значка', bar_color: 'Боја за лентата', bar_effect: 'Ефект на лентата', bar_orientation: 'Ориентација на лентата', bar_position: 'Позиција на лентата', bar_single_line: 'Инфо во еден ред (overlay)', bar_size: 'Големина на лента', center_zero: 'Нула во центарот', color: 'Примарна боја', decimal: 'децемален', disable_unit: 'Прикажи единица', double_tap_action: 'Дејство при двоен допир', entity: 'Ентитет', force_circular_background: 'Принуди кружна позадина', hide: 'Сокриј', hold_action: 'Дејство при долг допир', icon: 'Икона', icon_double_tap_action: 'Дејство при двоен допир на иконата', icon_hold_action: 'Дејство при долг допир на иконата', icon_tap_action: 'Дејство при допир на иконата', layout: 'Распоред на содржината', max_value: 'Максимална вредност', max_value_attribute: 'Атрибут (max_value)', max_value_entity: 'Максимална вредност', min_value: 'Минимална вредност', name: 'Име', percent: 'Процент', reverse_secondary_info_row: 'Сменете ги лентата и текстот', secondary: 'Секундарни информации', state_content: 'Содржина на состојба', tap_action: 'Дејство при краток допир', text_shadow: 'Додај сенка на текст (overlay)', theme: 'Тема', toggle_icon: 'Прикажи икона', toggle_name: 'Прикажи име', toggle_progress_bar: 'Прикажи лента на напредок', toggle_secondary_info: 'Прикажи секундарни информации', toggle_value: 'Прикажи вредност', unit: 'Јединство', use_max_entity: 'Користи ентитет за максимална вредност' }, option: { bar_orientation: { ltr: 'Лево кон десно', rtl: 'Десно кон лево', up: 'Нагоре (overlay)' }, bar_position: { below: 'Лента под содржината', bottom: 'Лента на дното (overlay)', default: 'Стандардно', overlay: 'Лента преку содржината (overlay)', top: 'Лента на врвот (overlay)', background: 'Позадина на картичката' }, bar_size: { large: 'Голема', medium: 'Средна', small: 'Мала', xlarge: 'Многу голема' }, layout: { horizontal: 'Хоризонтално (стандардно)', vertical: 'Вертикално' }, theme: { humidity: 'Влажност', light: 'Светлина', optimal_when_high: 'Оптимално кога е високо (Батерија...)', optimal_when_low: 'Оптимално кога е ниско(CPU, RAM,...)', pm25: 'PM2.5', temperature: 'Температура', voc: 'VOC' } } } }, nb: { card: { msg: { appliedDefaultValue: 'En standardverdi har blitt brukt automatisk.', attributeNotFound: 'Attributtet ble ikke funnet i Home Assistant.', discontinuousRange: 'Det definerte området er ikke sammenhengende.', entityNotFound: 'Enheten ble ikke funnet i Home Assistant.', invalidActionObject: 'Handlingsobjektet er ugyldig eller feil strukturert.', invalidCustomThemeArray: 'Tilpasset tema må være en array.', invalidCustomThemeEntry: 'Én eller flere oppføringer i temaet er ugyldige.', invalidDecimal: 'Verdien må være et gyldig desimaltall.', invalidEntityId: 'Enhets-ID er ugyldig eller feil formatert.', invalidEnumValue: 'Den oppgitte verdien er ikke en gyldig mulighet.', invalidIconType: 'Angitt ikon-type er ugyldig eller ukjent.', invalidMaxValue: 'Maksverdi er ugyldig eller for høy.', invalidMinValue: 'Minsteverdi er ugyldig eller for lav.', invalidStateContent: 'Tilstandsinnholdet er ugyldig eller feil formatert.', invalidStateContentEntry: 'En eller flere oppføringer i tilstandsinnholdet er ugyldige.', invalidTheme: 'Angitt tema er ukjent. Standardtema vil bli brukt.', invalidTypeArray: 'Forventet en verdi av typen array.', invalidTypeBoolean: 'Forventet en boolsk verdi.', invalidTypeNumber: 'Forventet en numerisk verdi.', invalidTypeObject: 'Forventet en verdi av typen objekt.', invalidTypeString: 'Forventet en verdi av typen string.', invalidUnionType: 'Verdien samsvarer ikke med noen av de tillatte typene.', minGreaterThanMax: 'Minsteverdi kan ikke være større enn maksverdi.', missingActionKey: 'En påkrevd nøkkel mangler i handlingsobjektet.', missingColorProperty: 'En nødvendig fargeegenskap mangler.', missingRequiredProperty: 'En påkrevd egenskap mangler.' } }, editor: { title: { content: 'Innhold', interaction: 'Interaksjoner', theme: 'Utseende og funksjonalitet' }, field: { attribute: 'Attributt', badge_color: 'Farge på merke', badge_icon: 'Ikon for merke', bar_color: 'Farge for baren', bar_effect: 'Effekt på baren', bar_orientation: 'Orientering av baren', bar_position: 'Posisjon for baren', bar_single_line: 'Info på én linje (overlay)', bar_size: 'Bar størrelse', center_zero: 'Null i midten', color: 'Primærfarge', decimal: 'desimal', disable_unit: 'Vis enhet', double_tap_action: 'Handling ved dobbelt trykk', entity: 'Enhet', force_circular_background: 'Tving sirkulær bakgrunn', hide: 'Skjul', hold_action: 'Handling ved langt trykk', icon: 'Ikon', icon_double_tap_action: 'Handling ved dobbelt trykk på ikonet', icon_hold_action: 'Handling ved langt trykk på ikonet', icon_tap_action: 'Handling ved trykk på ikonet', layout: 'Innholdslayout', max_value: 'Maksimal verdi', max_value_attribute: 'Attributt (max_value)', max_value_entity: 'Maksimal verdi', min_value: 'Minste verdi', name: 'Navn', percent: 'Prosent', reverse_secondary_info_row: 'Bytt linje og tekst', secondary: 'Sekundær informasjon', state_content: 'Innhold i tilstand', tap_action: 'Handling ved kort trykk', text_shadow: 'Legg til tekstskygge (overlay)', theme: 'Tema', toggle_icon: 'Vis ikon', toggle_name: 'Vis navn', toggle_progress_bar: 'Vis fremdriftsbar', toggle_secondary_info: 'Vis sekundær informasjon', toggle_value: 'Vis verdi', unit: 'Enhet', use_max_entity: 'Bruk enhet for maksimalverdi' }, option: { bar_orientation: { ltr: 'Venstre til høyre', rtl: 'Høyre til venstre', up: 'Oppover (overlay)' }, bar_position: { below: 'Bar under innholdet', bottom: 'Bar nederst (overlay)', default: 'Standard', overlay: 'Bar lagt over innholdet (overlay)', top: 'Bar øverst (overlay)', background: 'Kortbakgrunn' }, bar_size: { large: 'Stor', medium: 'Medium', small: 'Liten', xlarge: 'Ekstra stor' }, layout: { horizontal: 'Horisontal (standard)', vertical: 'Vertikal' }, theme: { humidity: 'Fuktighet', light: 'Lys', optimal_when_high: 'Optimal når høyt (Batteri...)', optimal_when_low: 'Optimal når lavt (CPU, RAM,...)', pm25: 'PM2.5', temperature: 'Temperatur', voc: 'VOC' } } } }, nl: { card: { msg: { appliedDefaultValue: 'Een standaardwaarde is automatisch toegepast.', attributeNotFound: 'Attribuut niet gevonden in Home Assistant.', discontinuousRange: 'Het opgegeven bereik is niet aaneengesloten.', entityNotFound: 'Entiteit niet gevonden in Home Assistant.', invalidActionObject: 'Het actieobject is ongeldig of onjuist gestructureerd.', invalidCustomThemeArray: 'Het aangepaste thema moet een array zijn.', invalidCustomThemeEntry: 'Een of meer invoeren in het aangepaste thema zijn ongeldig.', invalidDecimal: 'De waarde moet een geldig decimaal getal zijn.', invalidEntityId: 'De entity ID is ongeldig of foutief geformatteerd.', invalidEnumValue: 'De opgegeven waarde is geen geldige optie.', invalidIconType: 'Het opgegeven pictogramtype is ongeldig of niet herkend.', invalidMaxValue: 'De maximumwaarde is ongeldig of te hoog.', invalidMinValue: 'De minimumwaarde is ongeldig of te laag.', invalidStateContent: 'De statusinhoud is ongeldig of foutief.', invalidStateContentEntry: 'Een of meer onderdelen van de statusinhoud zijn ongeldig.', invalidTheme: 'Het opgegeven thema is onbekend. Het standaardthema wordt gebruikt.', invalidTypeArray: 'Verwachte waarde van het type array.', invalidTypeBoolean: 'Verwachte waarde van het type boolean.', invalidTypeNumber: 'Verwachte waarde van het type nummer.', invalidTypeObject: 'Verwachte waarde van het type object.', invalidTypeString: 'Verwachte waarde van het type string.', invalidUnionType: 'De waarde komt niet overeen met toegestane types.', minGreaterThanMax: 'Minimumwaarde kan niet groter zijn dan de maximumwaarde.', missingActionKey: 'Er ontbreekt een verplichte sleutel in het actieobject.', missingColorProperty: 'Een verplichte kleur-eigenschap ontbreekt.', missingRequiredProperty: 'Vereiste eigenschap ontbreekt.' } }, editor: { title: { content: 'Inhoud', interaction: 'Interactie', theme: 'Uiterlijk en gebruiksgemak' }, field: { attribute: 'Attribuut', badge_color: 'Kleur van badge', badge_icon: 'Pictogram van badge', bar_color: 'Kleur voor de balk', bar_effect: 'Effect op de balk', bar_orientation: 'Oriëntatie van de balk', bar_position: 'Positie van de balk', bar_single_line: 'Info op één regel (overlay)', bar_size: 'Balkgrootte', center_zero: 'Nul in het midden', color: 'Primaire kleur', decimal: 'decimaal', disable_unit: 'Eenheid weergeven', double_tap_action: 'Actie bij dubbel tikken', entity: 'Entiteit', force_circular_background: 'Geforceerde cirkelvormige achtergrond', hide: 'Verbergen', hold_action: 'Actie bij lang ingedrukt houden', icon: 'Pictogram', icon_double_tap_action: 'Actie bij dubbel tikken op pictogram', icon_hold_action: 'Actie bij lang ingedrukt houden op pictogram', icon_tap_action: 'Actie bij tikken op pictogram', layout: 'Inhoudsindeling', max_value: 'Maximale waarde', max_value_attribute: 'Attribuut (max_value)', max_value_entity: 'Maximale waarde', min_value: 'Minimale waarde', name: 'Naam', percent: 'Percentage', reverse_secondary_info_row: 'Balk en tekst omwisselen', secondary: 'Secundaire informatie', state_content: 'Inhoud van de status', tap_action: 'Actie bij korte tik', text_shadow: 'Tekstschaduw toevoegen (overlay)', theme: 'Thema', toggle_icon: 'Pictogram weergeven', toggle_name: 'Naam weergeven', toggle_progress_bar: 'Voortgangsbalk weergeven', toggle_secondary_info: 'Secundaire informatie weergeven', toggle_value: 'Waarde weergeven', unit: 'Eenheid', use_max_entity: 'Entiteit gebruiken voor maximale waarde' }, option: { bar_orientation: { ltr: 'Links naar rechts', rtl: 'Rechts naar links', up: 'Omhoog (overlay)' }, bar_position: { below: 'Balk onder de inhoud', bottom: 'Balk onderaan (overlay)', default: 'Standaard', overlay: 'Balk over de inhoud (overlay)', top: 'Balk bovenaan (overlay)', background: 'Kaartachtergrond' }, bar_size: { large: 'Groot', medium: 'Middel', small: 'Klein', xlarge: 'Extra groot' }, layout: { horizontal: 'Horizontaal (standaard)', vertical: 'Verticaal' }, theme: { humidity: 'Vochtigheid', light: 'Licht', optimal_when_high: 'Optimaal wanneer hoog (Batterij...)', optimal_when_low: 'Optimaal wanneer laag (CPU, RAM,...)', pm25: 'PM2.5', temperature: 'Temperatuur', voc: 'VOC' } } } }, pl: { card: { msg: { appliedDefaultValue: 'Zastosowano domyślną wartość automatycznie.', attributeNotFound: 'Nie znaleziono atrybutu w Home Assistant.', discontinuousRange: 'Zdefiniowany zakres jest nieciągły.', entityNotFound: 'Nie znaleziono encji w Home Assistant.', invalidActionObject: 'Obiekt akcji jest nieprawidłowy lub ma złą strukturę.', invalidCustomThemeArray: 'Własny motyw musi być tablicą.', invalidCustomThemeEntry: 'Jedna lub więcej pozycji motywu jest nieprawidłowa.', invalidDecimal: 'Wartość musi być poprawną liczbą dziesiętną.', invalidEntityId: 'ID encji jest nieprawidłowe lub ma zły format.', invalidEnumValue: 'Podana wartość nie jest jedną z dozwolonych opcji.', invalidIconType: 'Określony typ ikony jest nieprawidłowy lub nieznany.', invalidMaxValue: 'Maksymalna wartość jest nieprawidłowa lub zbyt wysoka.', invalidMinValue: 'Minimalna wartość jest nieprawidłowa lub zbyt niska.', invalidStateContent: 'Zawartość stanu jest nieprawidłowa lub uszkodzona.', invalidStateContentEntry: 'Jedna lub więcej pozycji zawartości stanu jest nieprawidłowa.', invalidTheme: 'Podany motyw jest nieznany. Zostanie użyty domyślny motyw.', invalidTypeArray: 'Oczekiwano wartości typu tablica.', invalidTypeBoolean: 'Oczekiwano wartości typu boolean.', invalidTypeNumber: 'Oczekiwano wartości typu liczba.', invalidTypeObject: 'Oczekiwano wartości typu obiekt.', invalidTypeString: 'Oczekiwano wartości typu string.', invalidUnionType: 'Wartość nie pasuje do żadnego z dozwolonych typów.', minGreaterThanMax: 'Wartość minimalna nie może być większa niż maksymalna.', missingActionKey: 'W obiekcie akcji brakuje wymaganej właściwości.', missingColorProperty: 'Brakuje wymaganej właściwości koloru.', missingRequiredProperty: 'Brakuje wymaganej właściwości.' } }, editor: { title: { content: 'Zawartość', interaction: 'Interakcje', theme: 'Wygląd i funkcjonalność' }, field: { attribute: 'Atrybut', badge_color: 'Kolor odznaki', badge_icon: 'Ikona odznaki', bar_color: 'Kolor paska', bar_effect: 'Efekt na pasku', bar_orientation: 'Orientacja paska', bar_position: 'Pozycja paska', bar_single_line: 'Info w jednej linii (overlay)', bar_size: 'Rozmiar paska', center_zero: 'Zero na środku', color: 'Kolor podstawowy', decimal: 'dziesiętny', disable_unit: 'Pokaż jednostkę', double_tap_action: 'Akcja przy podwójnym naciśnięciu', entity: 'Encja', force_circular_background: 'Wymuś okrągłe tło', hide: 'Ukryj', hold_action: 'Akcja przy długim naciśnięciu', icon: 'Ikona', icon_double_tap_action: 'Akcja przy podwójnym naciśnięciu ikony', icon_hold_action: 'Akcja przy długim naciśnięciu ikony', icon_tap_action: 'Akcja przy naciśnięciu ikony', layout: 'Układ treści', max_value: 'Wartość maksymalna', max_value_attribute: 'Atrybut (max_value)', max_value_entity: 'Wartość maksymalna', min_value: 'Wartość minimalna', name: 'Nazwa', percent: 'Procent', reverse_secondary_info_row: 'Zamień pasek i tekst', secondary: 'Informacja dodatkowa', state_content: 'Zawartość stanu', tap_action: 'Akcja przy krótkim naciśnięciu', text_shadow: 'Dodaj cień tekstu (overlay)', theme: 'Motyw', toggle_icon: 'Pokaż ikonę', toggle_name: 'Pokaż nazwę', toggle_progress_bar: 'Pokaż pasek postępu', toggle_secondary_info: 'Pokaż informację dodatkową', toggle_value: 'Pokaż wartość', unit: 'Jednostka', use_max_entity: 'Użyj encji dla wartości maksymalnej' }, option: { bar_orientation: { ltr: 'Od lewej do prawej', rtl: 'Od prawej do lewej', up: 'W górę (overlay)' }, bar_position: { below: 'Pasek pod zawartością', bottom: 'Pasek na dole (overlay)', default: 'Domyślnie', overlay: 'Pasek nałożony na zawartość (overlay)', top: 'Pasek na górze (overlay)', background: 'Tło karty' }, bar_size: { large: 'Duża', medium: 'Średnia', small: 'Mała', xlarge: 'Bardzo duża' }, layout: { horizontal: 'Poziomo (domyślnie)', vertical: 'Pionowy' }, theme: { humidity: 'Wilgotność', light: 'Światło', optimal_when_high: 'Optymalny, gdy wysokie (Bateria...)', optimal_when_low: 'Optymalny, gdy niskie (CPU, RAM,...)', pm25: 'PM2.5', temperature: 'Temperatura', voc: 'VOC' } } } }, 'pt-BR': { card: { msg: { appliedDefaultValue: 'Um valor padrão foi aplicado automaticamente.', attributeNotFound: 'Atributo não encontrado no Home Assistant.', discontinuousRange: 'O intervalo definido é descontínuo.', entityNotFound: 'Entidade não encontrada no Home Assistant.', invalidActionObject: 'O objeto de ação é inválido ou mal estruturado.', invalidCustomThemeArray: 'O tema personalizado deve ser um array.', invalidCustomThemeEntry: 'Uma ou mais entradas do tema personalizado são inválidas.', invalidDecimal: 'O valor deve ser um número inteiro positivo.', invalidEntityId: 'O ID da entidade é inválido ou mal formado.', invalidEnumValue: 'O valor fornecido não faz parte das opções permitidas.', invalidIconType: 'O tipo de ícone especificado é inválido ou desconhecido.', invalidMaxValue: 'O valor máximo é inválido ou excede os limites permitidos.', invalidMinValue: 'O valor mínimo é inválido ou abaixo dos limites permitidos.', invalidStateContent: 'O conteúdo do estado é inválido ou mal formado.', invalidStateContentEntry: 'Uma ou mais entradas do conteúdo do estado são inválidas.', invalidTheme: 'O tema especificado é desconhecido. O tema padrão será usado.', invalidTypeArray: 'Um valor do tipo array era esperado.', invalidTypeBoolean: 'Um valor do tipo booleano era esperado.', invalidTypeNumber: 'Um valor do tipo número era esperado.', invalidTypeObject: 'Um valor do tipo objeto era esperado.', invalidTypeString: 'Um valor do tipo string era esperado.', invalidUnionType: 'O valor não corresponde a nenhum dos tipos permitidos.', minGreaterThanMax: 'O valor mínimo não pode ser maior que o valor máximo.', missingActionKey: 'Uma chave obrigatória está ausente no objeto de ação.', missingColorProperty: 'Uma propriedade de cor obrigatória está ausente.', missingRequiredProperty: 'Uma propriedade obrigatória está ausente.' } }, editor: { title: { content: 'Conteúdo', interaction: 'Interações', theme: 'Aparência e usabilidade' }, field: { attribute: 'Atributo', badge_color: 'Cor do badge', badge_icon: 'Ícone do badge', bar_color: 'Cor da barra', bar_effect: 'Efeito na barra', bar_orientation: 'Orientação da barra', bar_position: 'Posição da barra', bar_single_line: 'Informações em uma linha (overlay)', bar_size: 'Tamanho da barra', center_zero: 'Zero no centro', color: 'Cor do ícone', decimal: 'Decimal', disable_unit: 'Mostrar unidade', double_tap_action: 'Ação ao tocar duas vezes', entity: 'Entidade', force_circular_background: 'Forçar fundo circular', hide: 'Ocultar', hold_action: 'Ação ao manter pressionado', icon: 'Ícone', icon_double_tap_action: 'Ação ao tocar duas vezes no ícone', icon_hold_action: 'Ação ao manter pressionado o ícone', icon_tap_action: 'Ação ao tocar no ícone', layout: 'Layout do conteúdo', max_value: 'Valor máximo', max_value_attribute: 'Atributo (max_value)', max_value_entity: 'Valor máximo', min_value: 'Valor mínimo', name: 'Nome', percent: 'Porcentagem', reverse_secondary_info_row: 'Trocar barra e texto', secondary: 'Informação secundária', state_content: 'Conteúdo do estado', tap_action: 'Ação ao tocar', text_shadow: 'Adicionar sombra ao texto (overlay)', theme: 'Tema', toggle_icon: 'Mostrar ícone', toggle_name: 'Mostrar nome', toggle_progress_bar: 'Mostrar barra de progresso', toggle_secondary_info: 'Mostrar informações secundárias', toggle_value: 'Mostrar valor', unit: 'Unidade', use_max_entity: 'Usar entidade para valor máximo' }, option: { theme: { optimal_when_low: 'Ideal quando baixo (CPU, RAM...)', optimal_when_high: 'Ideal quando alto (Bateria...)', light: 'Claro', temperature: 'Temperatura', humidity: 'Umidade', pm25: 'PM2.5', voc: 'VOC' }, bar_size: { small: 'Pequena', medium: 'Média', large: 'Grande', xlarge: 'Muito grande' }, bar_orientation: { ltr: 'Esquerda para direita', rtl: 'Direita para esquerda', up: 'Para cima (overlay)' }, bar_position: { default: 'Padrão', below: 'Barra abaixo do conteúdo', top: 'Barra acima (overlay)', bottom: 'Barra abaixo (overlay)', overlay: 'Barra sobre o conteúdo (overlay)', background: 'Fundo do cartão' }, layout: { horizontal: 'Horizontal (padrão)', vertical: 'Vertical' } } } }, pt: { card: { msg: { appliedDefaultValue: 'Um valor padrão foi aplicado automaticamente.', attributeNotFound: 'Atributo não encontrado no Home Assistant.', discontinuousRange: 'O intervalo definido é descontínuo.', entityNotFound: 'Entidade não encontrada no Home Assistant.', invalidActionObject: 'O objeto de ação é inválido ou mal estruturado.', invalidCustomThemeArray: 'O tema personalizado deve ser um array.', invalidCustomThemeEntry: 'Uma ou mais entradas no tema personalizado são inválidas.', invalidDecimal: 'O valor deve ser um número decimal válido.', invalidEntityId: 'O ID da entidade é inválido ou mal formatado.', invalidEnumValue: 'O valor fornecido não é uma opção válida.', invalidIconType: 'O tipo de ícone especificado é inválido ou desconhecido.', invalidMaxValue: 'O valor máximo é inválido ou acima do permitido.', invalidMinValue: 'O valor mínimo é inválido ou abaixo do permitido.', invalidStateContent: 'O conteúdo do estado é inválido ou mal formatado.', invalidStateContentEntry: 'Uma ou mais entradas do conteúdo do estado são inválidas.', invalidTheme: 'O tema especificado é desconhecido. Tema padrão será usado.', invalidTypeArray: 'Esperava-se um valor do tipo array.', invalidTypeBoolean: 'Esperava-se um valor do tipo booleano.', invalidTypeNumber: 'Esperava-se um valor do tipo número.', invalidTypeObject: 'Esperava-se um valor do tipo objeto.', invalidTypeString: 'Esperava-se um valor do tipo string.', invalidUnionType: 'O valor não corresponde a nenhum dos tipos permitidos.', minGreaterThanMax: 'O valor mínimo não pode ser maior que o valor máximo.', missingActionKey: 'Uma chave obrigatória está faltando no objeto de ação.', missingColorProperty: 'Uma propriedade de cor obrigatória está faltando.', missingRequiredProperty: 'Propriedade obrigatória ausente.' } }, editor: { title: { content: 'Conteúdo', interaction: 'Interações', theme: 'Aparência e usabilidade' }, field: { attribute: 'Atributo', badge_color: 'Cor do crachá', badge_icon: 'Ícone do crachá', bar_color: 'Cor para a barra', bar_effect: 'Efeito na barra', bar_orientation: 'Orientação da barra', bar_position: 'Posição da barra', bar_single_line: 'Info numa só linha (overlay)', bar_size: 'Tamanho da barra', center_zero: 'Zero ao centro', color: 'Cor primária', decimal: 'decimal', disable_unit: 'Mostrar unidade', double_tap_action: 'Ação ao toque duplo', entity: 'Entidade', force_circular_background: 'Forçar fundo circular', hide: 'Ocultar', hold_action: 'Ação ao toque longo', icon: 'Ícone', icon_double_tap_action: 'Ação ao tocar duplamente no ícone', icon_hold_action: 'Ação ao manter o toque no ícone', icon_tap_action: 'Ação ao tocar no ícone', layout: 'Layout do conteúdo', max_value: 'Valor máximo', max_value_attribute: 'Atributo (max_value)', max_value_entity: 'Valor máximo', min_value: 'Valor mínimo', name: 'Nome', percent: 'Percentagem', reverse_secondary_info_row: 'Trocar barra e texto', secondary: 'Informação secundária', state_content: 'Conteúdo do estado', tap_action: 'Ação ao toque curto', text_shadow: 'Adicionar sombra ao texto (overlay)', theme: 'Tema', toggle_icon: 'Mostrar ícone', toggle_name: 'Mostrar nome', toggle_progress_bar: 'Mostrar barra de progresso', toggle_secondary_info: 'Mostrar informação secundária', toggle_value: 'Mostrar valor', unit: 'Unidade', use_max_entity: 'Usar entidade para o valor máximo' }, option: { bar_orientation: { ltr: 'Da esquerda para a direita', rtl: 'Da direita para a esquerda', up: 'Para cima (overlay)' }, bar_position: { below: 'Barra abaixo do conteúdo', bottom: 'Barra em baixo (overlay)', default: 'Padrão', overlay: 'Barra sobreposta ao conteúdo (overlay)', top: 'Barra em cima (overlay)', background: 'Fundo do cartão' }, bar_size: { large: 'Grande', medium: 'Média', small: 'Pequena', xlarge: 'Extra grande' }, layout: { horizontal: 'Horizontal (padrão)', vertical: 'Vertical' }, theme: { humidity: 'Humidade', light: 'Luz', optimal_when_high: 'Ótimo quando é alto (Bateria...)', optimal_when_low: 'Ótimo quando é baixo (CPU, RAM,...)', pm25: 'PM2.5', temperature: 'Temperatura', voc: 'VOC' } } } }, ro: { card: { msg: { appliedDefaultValue: 'A fost aplicată automat o valoare implicită.', attributeNotFound: 'Atributul nu a fost găsit în Home Assistant.', discontinuousRange: 'Intervalul definit este discontinuu.', entityNotFound: 'Entitatea nu a fost găsită în Home Assistant.', invalidActionObject: 'Obiectul acțiune este invalid sau structurat incorect.', invalidCustomThemeArray: 'Tema personalizată trebuie să fie un array.', invalidCustomThemeEntry: 'Una sau mai multe intrări din temă sunt invalide.', invalidDecimal: 'Valoarea trebuie să fie un număr zecimal valid.', invalidEntityId: 'ID-ul entității este invalid sau formatat greșit.', invalidEnumValue: 'Valoarea furnizată nu este una dintre opțiunile permise.', invalidIconType: 'Tipul de pictogramă specificat este invalid sau necunoscut.', invalidMaxValue: 'Valoarea maximă este invalidă sau prea mare.', invalidMinValue: 'Valoarea minimă este invalidă sau prea mică.', invalidStateContent: 'Conținutul stării este invalid sau formatat greșit.', invalidStateContentEntry: 'Una sau mai multe intrări în conținutul stării sunt invalide.', invalidTheme: 'Tema specificată este necunoscută. Va fi utilizată tema implicită.', invalidTypeArray: 'Se aștepta o valoare de tip array.', invalidTypeBoolean: 'Se aștepta o valoare de tip boolean.', invalidTypeNumber: 'Se aștepta o valoare de tip număr.', invalidTypeObject: 'Se aștepta o valoare de tip obiect.', invalidTypeString: 'Se aștepta o valoare de tip șir.', invalidUnionType: 'Valoarea nu se potrivește niciunui tip permis.', minGreaterThanMax: 'Valoarea minimă nu poate fi mai mare decât valoarea maximă.', missingActionKey: 'Lipsește o cheie necesară în obiectul acțiune.', missingColorProperty: 'Lipsește o proprietate de culoare necesară.', missingRequiredProperty: 'Lipsește o proprietate necesară.' } }, editor: { title: { content: 'Conținut', interaction: 'Interacțiuni', theme: 'Aspect & Stil' }, field: { attribute: 'Atribut', badge_color: 'Culoare insignă', badge_icon: 'Pictogramă insignă', bar_color: 'Culoare bară', bar_effect: 'Efect pe bară', bar_orientation: 'Orientarea barei', bar_position: 'Poziția barei', bar_single_line: 'Info pe un singur rând (overlay)', bar_size: 'Dimensiunea barei', center_zero: 'Zero la centru', color: 'Culoare principală', decimal: 'zecimal', disable_unit: 'Afișează unitatea', double_tap_action: 'Acțiune la apăsare dublă', entity: 'Entitate', force_circular_background: 'Forțează fundal circular', hide: 'Ascunde', hold_action: 'Acțiune la apăsare lungă', icon: 'Pictogramă', icon_double_tap_action: 'Acțiune la apăsare dublă a pictogramei', icon_hold_action: 'Acțiune la apăsare lungă a pictogramei', icon_tap_action: 'Acțiune la apăsarea pictogramei', layout: 'Aspect conținut', max_value: 'Valoare maximă', max_value_attribute: 'Atribut (max_value)', max_value_entity: 'Valoare maximă', min_value: 'Valoare minimă', name: 'Nume', percent: 'Procent', reverse_secondary_info_row: 'Schimbați bara și textul', secondary: 'Informație secundară', state_content: 'Conținutul stării', tap_action: 'Acțiune la apăsare scurtă', text_shadow: 'Adaugă umbră textului (overlay)', theme: 'Temă', toggle_icon: 'Afișează pictograma', toggle_name: 'Afișează numele', toggle_progress_bar: 'Afișează bara de progres', toggle_secondary_info: 'Afișează informația secundară', toggle_value: 'Afișează valoarea', unit: 'Unitate', use_max_entity: 'Folosește entitate pentru valoarea maximă' }, option: { bar_orientation: { ltr: 'De la stânga la dreapta', rtl: 'De la dreapta la stânga', up: 'În sus (overlay)' }, bar_position: { below: 'Bară sub conținut', bottom: 'Bară jos (overlay)', default: 'Implicit', overlay: 'Bară suprapusă peste conținut (overlay)', top: 'Bară sus (overlay)', background: 'Fundal card' }, bar_size: { large: 'Mare', medium: 'Medie', small: 'Mică', xlarge: 'Foarte mare' }, layout: { horizontal: 'Orizontal (implicit)', vertical: 'Vertical' }, theme: { humidity: 'Umiditate', light: 'Luminozitate', optimal_when_high: 'Optim când este ridicat (Baterie...)', optimal_when_low: 'Optim când este scăzut (CPU, RAM...)', pm25: 'PM2.5', temperature: 'Temperatură', voc: 'VOC' } } } }, ru: { card: { msg: { appliedDefaultValue: 'Значение по умолчанию было применено автоматически.', attributeNotFound: 'Атрибут не найден в Home Assistant.', discontinuousRange: 'Определённый диапазон является прерывистым.', entityNotFound: 'Сущность не найдена в Home Assistant.', invalidActionObject: 'Объект действия недействителен или неправильно структурирован.', invalidCustomThemeArray: 'Пользовательская тема должна быть массивом.', invalidCustomThemeEntry: 'Одна или несколько записей в пользовательской теме недействительны.', invalidDecimal: 'Значение должно быть действительным десятичным числом.', invalidEntityId: 'Идентификатор сущности недействителен или неправильно сформирован.', invalidEnumValue: 'Предоставленное значение не является одним из разрешённых вариантов.', invalidIconType: 'Указанный тип иконки недействителен или не распознан.', invalidMaxValue: 'Максимальное значение недействительно или выше разрешённых пределов.', invalidMinValue: 'Минимальное значение недействительно или ниже разрешённых пределов.', invalidStateContent: 'Содержимое состояния недействительно или неправильно сформировано.', invalidStateContentEntry: 'Одна или несколько записей в содержимом состояния недействительны.', invalidTheme: 'Указанная тема неизвестна. Будет использована тема по умолчанию.', invalidTypeArray: 'Ожидается значение типа массив.', invalidTypeBoolean: 'Ожидается значение логического типа.', invalidTypeNumber: 'Ожидается значение числового типа.', invalidTypeObject: 'Ожидается значение типа объект.', invalidTypeString: 'Ожидается значение строкового типа.', invalidUnionType: 'Значение не соответствует ни одному из разрешённых типов.', minGreaterThanMax: 'Минимальное значение не может быть больше максимального значения.', missingActionKey: 'В объекте действия отсутствует обязательный ключ.', missingColorProperty: 'Отсутствует обязательное свойство цвета.', missingRequiredProperty: 'Отсутствует обязательное свойство.' } }, editor: { title: { content: 'Содержимое', interaction: 'Взаимодействия', theme: 'Внешний вид' }, field: { attribute: 'Атрибут', badge_color: 'Цвет значка', badge_icon: 'Иконка значка', bar_color: 'Цвет полосы', bar_effect: 'Эффект на полосе', bar_orientation: 'Ориентация полосы', bar_position: 'Положение полосы', bar_single_line: 'Информация в одну строку (overlay)', bar_size: 'Размер полосы', center_zero: 'Ноль по центру', color: 'Основной цвет', decimal: 'десятичные', disable_unit: 'Показать единицу измерения', double_tap_action: 'Поведение при двойном нажатии', entity: 'Сущность', force_circular_background: 'Принудительный круглый фон', hide: 'Скрыть', hold_action: 'Поведение при длительном нажатии', icon: 'Иконка', icon_double_tap_action: 'Поведение при двойном нажатии на иконку', icon_hold_action: 'Поведение при длительном нажатии на иконку', icon_tap_action: 'Поведение при нажатии на иконку', layout: 'Расположение содержимого', max_value: 'Максимальное значение', max_value_attribute: 'Атрибут (max_value)', max_value_entity: 'Максимальное значение', min_value: 'Минимальное значение', name: 'Название', percent: 'Процент', reverse_secondary_info_row: 'Поменять местами панель и текст', secondary: 'Дополнительная информация', state_content: 'Содержимое состояния', tap_action: 'Поведение при нажатии', text_shadow: 'Добавить тень к тексту (overlay)', theme: 'Тема', toggle_icon: 'Показать иконку', toggle_name: 'Показать название', toggle_progress_bar: 'Показать полосу прогресса', toggle_secondary_info: 'Показать дополнительную информацию', toggle_value: 'Показать значение', unit: 'Единица измерения', use_max_entity: 'Использовать сущность для максимального значения' }, option: { bar_orientation: { ltr: 'Слева направо', rtl: 'Справа налево', up: 'Вверх (overlay)' }, bar_position: { below: 'Полоса под содержимым', bottom: 'Полоса внизу (overlay)', default: 'По умолчанию', overlay: 'Полоса поверх содержимого (overlay)', top: 'Полоса вверху (overlay)', background: 'Фон карточки' }, bar_size: { large: 'Большая', medium: 'Средняя', small: 'Маленькая', xlarge: 'Очень большая' }, layout: { horizontal: 'Горизонтальный (по умолчанию)', vertical: 'Вертикальный' }, theme: { humidity: 'Влажность', light: 'Освещение', optimal_when_high: 'Оптимально при высоких значениях (Батарея...)', optimal_when_low: 'Оптимально при низких значениях (ЦП, ОЗУ,...)', pm25: 'PM2.5', temperature: 'Температура', voc: 'ЛОС' } } } }, sk: { card: { msg: { appliedDefaultValue: 'Predvolená hodnota bola automaticky použitá.', attributeNotFound: 'Atribút sa nenašiel v Home Assistant.', discontinuousRange: 'Zadaný rozsah nie je spojitý.', entityNotFound: 'Entita sa nenašla v Home Assistant.', invalidActionObject: 'Objekt akcie je neplatný alebo nesprávne štruktúrovaný.', invalidCustomThemeArray: 'Vlastná téma musí byť pole.', invalidCustomThemeEntry: 'Jedna alebo viac položiek vlastnej témy je neplatných.', invalidDecimal: 'Hodnota musí byť kladné celé číslo.', invalidEntityId: 'ID entity je neplatné alebo nesprávne.', invalidEnumValue: 'Zadaná hodnota nie je súčasťou povolených možností.', invalidIconType: 'Zadaný typ ikony je neplatný alebo neznámy.', invalidMaxValue: 'Maximálna hodnota je neplatná alebo nad limitom.', invalidMinValue: 'Minimálna hodnota je neplatná alebo pod limitom.', invalidStateContent: 'Obsah stavu je neplatný alebo nesprávny.', invalidStateContentEntry: 'Jedna alebo viac položiek obsahu stavu je neplatných.', invalidTheme: 'Zadaná téma je neznáma. Použije sa predvolená téma.', invalidTypeArray: 'Očakávala sa hodnota typu pole.', invalidTypeBoolean: 'Očakávala sa hodnota typu boolean.', invalidTypeNumber: 'Očakávala sa hodnota typu číslo.', invalidTypeObject: 'Očakávala sa hodnota typu objekt.', invalidTypeString: 'Očakávala sa hodnota typu reťazec.', invalidUnionType: 'Hodnota nezodpovedá žiadnemu povolenému typu.', minGreaterThanMax: 'Minimálna hodnota nemôže byť väčšia ako maximálna.', missingActionKey: 'Chýba povinný kľúč v objekte akcie.', missingColorProperty: 'Chýba povinná vlastnosť farby.', missingRequiredProperty: 'Chýba povinná vlastnosť.' } }, editor: { title: { content: 'Obsah', interaction: 'Interakcie', theme: 'Vzhľad a použiteľnosť' }, field: { attribute: 'Atribút', badge_color: 'Farba odznaku', badge_icon: 'Ikona odznaku', bar_color: 'Farba lišty', bar_effect: 'Efekt lišty', bar_orientation: 'Orientácia lišty', bar_position: 'Pozícia lišty', bar_single_line: 'Informácie na jednej línii (overlay)', bar_size: 'Veľkosť lišty', center_zero: 'Nula v strede', color: 'Farba ikony', decimal: 'Desatinné', disable_unit: 'Zobraziť jednotku', double_tap_action: 'Akcia pri dvojitom ťuknutí', entity: 'Entita', force_circular_background: 'Vynútiť kruhové pozadie', hide: 'Skryť', hold_action: 'Akcia pri dlhom podržaní', icon: 'Ikona', icon_double_tap_action: 'Akcia pri dvojitom ťuknutí ikony', icon_hold_action: 'Akcia pri dlhom podržaní ikony', icon_tap_action: 'Akcia pri ťuknutí ikony', layout: 'Rozloženie obsahu', max_value: 'Maximálna hodnota', max_value_attribute: 'Atribút (max_value)', max_value_entity: 'Maximálna hodnota', min_value: 'Minimálna hodnota', name: 'Názov', percent: 'Percento', reverse_secondary_info_row: 'Vymeňte lištu a text', secondary: 'Sekundárna informácia', state_content: 'Obsah stavu', tap_action: 'Akcia pri ťuknutí', text_shadow: 'Pridať tieň textu (overlay)', theme: 'Téma', toggle_icon: 'Zobraziť ikonu', toggle_name: 'Zobraziť názov', toggle_progress_bar: 'Zobraziť pruh postupu', toggle_secondary_info: 'Zobraziť sekundárne info', toggle_value: 'Zobraziť hodnotu', unit: 'Jednotka', use_max_entity: 'Použiť entitu pre max hodnotu' }, option: { theme: { optimal_when_low: 'Optimálne pri nízkej hodnote (CPU, RAM...)', optimal_when_high: 'Optimálne pri vysokej hodnote (Batéria...)', light: 'Svetlá', temperature: 'Teplota', humidity: 'Vlhkosť', pm25: 'PM2.5', voc: 'VOC' }, bar_size: { small: 'Malá', medium: 'Stredná', large: 'Veľká', xlarge: 'Veľmi veľká' }, bar_orientation: { ltr: 'Zľava doprava', rtl: 'Sprava doľava', up: 'Nahor (overlay)' }, bar_position: { default: 'Predvolené', below: 'Pruh pod obsahom', top: 'Pruh hore (overlay)', bottom: 'Pruh dole (overlay)', overlay: 'Pruh cez obsah (overlay)', background: 'Pozadie karty' }, layout: { horizontal: 'Horizontálne (predvolené)', vertical: 'Vertikálne' } } } }, sl: { card: { msg: { appliedDefaultValue: 'Privzeta vrednost je bila samodejno uporabljena.', attributeNotFound: 'Atribut ni bil najden v Home Assistant.', discontinuousRange: 'Določeno območje ni neprekinjeno.', entityNotFound: 'Entiteta ni bila najdena v Home Assistant.', invalidActionObject: 'Objekt akcije je neveljaven ali napačno strukturiran.', invalidCustomThemeArray: 'Prilagojena tema mora biti polje.', invalidCustomThemeEntry: 'Ena ali več vnosov prilagojene teme je neveljavnih.', invalidDecimal: 'Vrednost mora biti pozitivno celo število.', invalidEntityId: 'ID entitete je neveljaven ali napačen.', invalidEnumValue: 'Podana vrednost ni med dovoljenimi možnostmi.', invalidIconType: 'Podana vrsta ikone je neveljavna ali neznana.', invalidMaxValue: 'Največja vrednost je neveljavna ali presega omejitve.', invalidMinValue: 'Najmanjša vrednost je neveljavna ali pod dovoljenimi mejami.', invalidStateContent: 'Vsebina stanja je neveljavna ali napačno oblikovana.', invalidStateContentEntry: 'Eden ali več vnosov vsebine stanja je neveljavno.', invalidTheme: 'Določena tema je neznana. Uporabila se bo privzeta tema.', invalidTypeArray: 'Pričakovana je bila vrednost tipa polje.', invalidTypeBoolean: 'Pričakovana je bila vrednost tipa boolean.', invalidTypeNumber: 'Pričakovana je bila vrednost tipa število.', invalidTypeObject: 'Pričakovana je bila vrednost tipa objekt.', invalidTypeString: 'Pričakovana je bila vrednost tipa niz.', invalidUnionType: 'Vrednost ne ustreza nobeni dovoljeni vrsti.', minGreaterThanMax: 'Najmanjša vrednost ne sme biti večja od največje.', missingActionKey: 'Manjka obvezni ključ v objektu akcije.', missingColorProperty: 'Manjka obvezna lastnost barve.', missingRequiredProperty: 'Manjka obvezna lastnost.' } }, editor: { title: { content: 'Vsebina', interaction: 'Interakcije', theme: 'Videz in uporabnost' }, field: { attribute: 'Atribut', badge_color: 'Barva značke', badge_icon: 'Ikona značke', bar_color: 'Barva vrstice', bar_effect: 'Učinek vrstice', bar_orientation: 'Usmeritev vrstice', bar_position: 'Pozicija vrstice', bar_single_line: 'Informacije v eni vrstici (overlay)', bar_size: 'Velikost vrstice', center_zero: 'Ni ničle na sredini', color: 'Barva ikone', decimal: 'Decimalno', disable_unit: 'Prikaži enoto', double_tap_action: 'Akcija ob dvojni tap', entity: 'Entiteta', force_circular_background: 'Prisili krožno ozadje', hide: 'Skrij', hold_action: 'Akcija ob dolgem pritisku', icon: 'Ikona', icon_double_tap_action: 'Akcija ob dvojni tap ikone', icon_hold_action: 'Akcija ob dolgem pritisku ikone', icon_tap_action: 'Akcija ob tap ikone', layout: 'Postavitev vsebine', max_value: 'Največja vrednost', max_value_attribute: 'Atribut (max_value)', max_value_entity: 'Največja vrednost', min_value: 'Najmanjša vrednost', name: 'Ime', percent: 'Odstotek', reverse_secondary_info_row: 'Zamenjaj vrstico in besedilo', secondary: 'Sekundarne informacije', state_content: 'Vsebina stanja', tap_action: 'Akcija ob tap', text_shadow: 'Dodaj senco besedila (overlay)', theme: 'Tema', toggle_icon: 'Prikaži ikono', toggle_name: 'Prikaži ime', toggle_progress_bar: 'Prikaži vrstico napredka', toggle_secondary_info: 'Prikaži sekundarne informacije', toggle_value: 'Prikaži vrednost', unit: 'Enota', use_max_entity: 'Uporabi entiteto za max vrednost' }, option: { theme: { optimal_when_low: 'Optimalno pri nizkih vrednostih (CPU, RAM...)', optimal_when_high: 'Optimalno pri visokih vrednostih (Baterija...)', light: 'Svetla', temperature: 'Temperatura', humidity: 'Vlažnost', pm25: 'PM2.5', voc: 'VOC' }, bar_size: { small: 'Majhna', medium: 'Srednja', large: 'Velika', xlarge: 'Zelo velika' }, bar_orientation: { ltr: 'Levo proti desni', rtl: 'Desno proti levi', up: 'Navzgor (overlay)' }, bar_position: { default: 'Privzeto', below: 'Vrstica pod vsebino', top: 'Vrstica zgoraj (overlay)', bottom: 'Vrstica spodaj (overlay)', overlay: 'Vrstica čez vsebino (overlay)', background: 'Ozadje kartice' }, layout: { horizontal: 'Horizontalno (privzeto)', vertical: 'Vertikalno' } } } }, sv: { card: { msg: { appliedDefaultValue: 'Ett standardvärde har tillämpats automatiskt.', attributeNotFound: 'Attributet kunde inte hittas i Home Assistant.', discontinuousRange: 'Det angivna intervallet är inte sammanhängande.', entityNotFound: 'Enheten kunde inte hittas i Home Assistant.', invalidActionObject: 'Åtgärdsobjektet är ogiltigt eller felstrukturerat.', invalidCustomThemeArray: 'Det anpassade temat måste vara en array.', invalidCustomThemeEntry: 'En eller flera poster i det anpassade temat är ogiltiga.', invalidDecimal: 'Värdet måste vara ett giltigt decimaltal.', invalidEntityId: 'Enhets-ID är ogiltigt eller felaktigt formaterat.', invalidEnumValue: 'Det angivna värdet är inte ett giltigt alternativ.', invalidIconType: 'Den angivna ikontypen är ogiltig eller okänd.', invalidMaxValue: 'Maximivärdet är ogiltigt eller för högt.', invalidMinValue: 'Minimivärdet är ogiltigt eller för lågt.', invalidStateContent: 'Tillståndsinnehållet är ogiltigt eller felaktigt.', invalidStateContentEntry: 'En eller flera poster i tillståndsinnehållet är ogiltiga.', invalidTheme: 'Det angivna temat är okänt. Standardtema används.', invalidTypeArray: 'Förväntade ett värde av typen array.', invalidTypeBoolean: 'Förväntade ett värde av typen boolean.', invalidTypeNumber: 'Förväntade ett värde av typen nummer.', invalidTypeObject: 'Förväntade ett värde av typen objekt.', invalidTypeString: 'Förväntade ett värde av typen sträng.', invalidUnionType: 'Värdet matchar inte något av de tillåtna typerna.', minGreaterThanMax: 'Minimivärdet kan inte vara större än maximivärdet.', missingActionKey: 'En obligatorisk nyckel saknas i åtgärdsobjektet.', missingColorProperty: 'En obligatorisk färgegenskap saknas.', missingRequiredProperty: 'En obligatorisk egenskap saknas.' } }, editor: { title: { content: 'Innehåll', interaction: 'Interaktioner', theme: 'Utseende och funktionalitet' }, field: { attribute: 'Attribut', badge_color: 'Färg på bricka', badge_icon: 'Ikon för bricka', bar_color: 'Färg för baren', bar_effect: 'Effekt på baren', bar_orientation: 'Orientering av baren', bar_position: 'Position för baren', bar_single_line: 'Info på en rad (overlay)', bar_size: 'Barstorlek', center_zero: 'Noll i mitten', color: 'Primärfärg', decimal: 'decimal', disable_unit: 'Visa enhet', double_tap_action: 'Åtgärd vid dubbeltryck', entity: 'Enhet', force_circular_background: 'Tvinga cirkulär bakgrund', hide: 'Dölj', hold_action: 'Åtgärd vid långt tryck', icon: 'Ikon', icon_double_tap_action: 'Åtgärd vid dubbeltryck på ikonen', icon_hold_action: 'Åtgärd vid långt tryck på ikonen', icon_tap_action: 'Åtgärd vid tryck på ikonen', layout: 'Innehållslayout', max_value: 'Maximalt värde', max_value_attribute: 'Attribut (max_value)', max_value_entity: 'Maximalt värde', min_value: 'Minsta värde', name: 'Namn', percent: 'Procent', reverse_secondary_info_row: 'Byt ut stapel och text', secondary: 'Sekundär information', state_content: 'Statusinnehåll', tap_action: 'Åtgärd vid kort tryck', text_shadow: 'Lägg till textskugga (overlay)', theme: 'Tema', toggle_icon: 'Visa ikon', toggle_name: 'Visa namn', toggle_progress_bar: 'Visa förloppsfält', toggle_secondary_info: 'Visa sekundär information', toggle_value: 'Visa värde', unit: 'Enhet', use_max_entity: 'Använd enhet för maximalt värde' }, option: { bar_orientation: { ltr: 'Vänster till höger', rtl: 'Höger till vänster', up: 'Uppåt (overlay)' }, bar_position: { below: 'Bar under innehållet', bottom: 'Bar längst ned (overlay)', default: 'Standard', overlay: 'Bar överlagrad på innehållet (overlay)', top: 'Bar längst upp (overlay)', background: 'Kortbakgrund' }, bar_size: { large: 'Stor', medium: 'Medium', small: 'Liten', xlarge: 'Extra stor' }, layout: { horizontal: 'Horisontell (standard)', vertical: 'Vertikal' }, theme: { humidity: 'Luftfuktighet', light: 'Ljus', optimal_when_high: 'Optimal när det är högt (Batteri...)', optimal_when_low: 'Optimal när det är lågt (CPU, RAM,...)', pm25: 'PM2.5', temperature: 'Temperatur', voc: 'VOC' } } } }, template: { card: { msg: { appliedDefaultValue: '', attributeNotFound: '', discontinuousRange: '', entityNotFound: '', invalidActionObject: '', invalidCustomThemeArray: '', invalidCustomThemeEntry: '', invalidDecimal: '', invalidEntityId: '', invalidEnumValue: '', invalidIconType: '', invalidMaxValue: '', invalidMinValue: '', invalidStateContent: '', invalidStateContentEntry: '', invalidTheme: '', invalidTypeArray: '', invalidTypeBoolean: '', invalidTypeNumber: '', invalidTypeObject: '', invalidTypeString: '', invalidUnionType: '', minGreaterThanMax: '', missingActionKey: '', missingColorProperty: '', missingRequiredProperty: '' } }, editor: { title: { content: '', interaction: '', theme: '' }, field: { attribute: '', badge_color: '', badge_icon: '', bar_color: '', bar_effect: '', bar_orientation: '', bar_position: '', bar_single_line: '', bar_size: '', center_zero: '', color: '', decimal: '', disable_unit: '', double_tap_action: '', entity: '', force_circular_background: '', hide: 'true', hold_action: '', icon: '', icon_double_tap_action: '', icon_hold_action: '', icon_tap_action: '', layout: '', max_value: '', max_value_attribute: '', max_value_entity: '', min_value: '', name: '', percent: '', reverse_secondary_info_row: '', secondary: '', state_content: '', tap_action: '', text_shadow: '', theme: '', toggle_icon: '', toggle_name: '', toggle_progress_bar: '', toggle_secondary_info: '', toggle_value: '', unit: '', use_max_entity: '' }, option: { theme: { optimal_when_low: '', optimal_when_high: '', light: '', temperature: '', humidity: '', pm25: '', voc: '' }, bar_size: { small: '', medium: '', large: '', xlarge: '' }, bar_orientation: { ltr: '', rtl: '', up: '' }, bar_position: { default: '', below: '', top: '', bottom: '', overlay: '', background: '' }, layout: { horizontal: '', vertical: '' } } } }, th: { card: { msg: { appliedDefaultValue: 'ค่าเริ่มต้นถูกนำไปใช้โดยอัตโนมัติ', attributeNotFound: 'ไม่พบแอตทริบิวต์ใน HA', discontinuousRange: 'ช่วงที่กำหนดไม่ต่อเนื่อง', entityNotFound: 'ไม่พบเอนทิตีใน HA', invalidActionObject: 'ออบเจ็กต์แอ็กชันไม่ถูกต้องหรือโครงสร้างผิด', invalidCustomThemeArray: 'ธีมกำหนดเองต้องเป็นอาร์เรย์', invalidCustomThemeEntry: 'หนึ่งหรือหลายรายการในธีมกำหนดเองไม่ถูกต้อง', invalidDecimal: 'ค่าต้องเป็นตัวเลขทศนิยมที่ถูกต้อง', invalidEntityId: 'ID เอนทิตีไม่ถูกต้องหรือรูปแบบผิด', invalidEnumValue: 'ค่าที่ให้มาไม่ใช่หนึ่งในตัวเลือกที่อนุญาต', invalidIconType: 'ประเภทไอคอนที่ระบุไม่ถูกต้องหรือไม่รู้จัก', invalidMaxValue: 'ค่าสูงสุดไม่ถูกต้องหรือสูงกว่าขีดจำกัดที่อนุญาต', invalidMinValue: 'ค่าต่ำสุดไม่ถูกต้องหรือต่ำกว่าขีดจำกัดที่อนุญาต', invalidStateContent: 'เนื้อหาสถานะไม่ถูกต้องหรือรูปแบบผิด', invalidStateContentEntry: 'หนึ่งหรือหลายรายการในเนื้อหาสถานะไม่ถูกต้อง', invalidTheme: 'ธีมที่ระบุไม่รู้จัก จะใช้ธีมเริ่มต้น', invalidTypeArray: 'คาดหวังค่าประเภทอาร์เรย์', invalidTypeBoolean: 'คาดหวังค่าประเภทบูลีน', invalidTypeNumber: 'คาดหวังค่าประเภทตัวเลข', invalidTypeObject: 'คาดหวังค่าประเภทออบเจ็กต์', invalidTypeString: 'คาดหวังค่าประเภทสตริง', invalidUnionType: 'ค่าไม่ตรงกับประเภทที่อนุญาตใด ๆ', minGreaterThanMax: 'ค่าต่ำสุดไม่สามารถมากกว่าค่าสูงสุด', missingActionKey: 'ขาดคีย์ที่จำเป็นในออบเจ็กต์แอ็กชัน', missingColorProperty: 'ขาดคุณสมบัติสีที่จำเป็น', missingRequiredProperty: 'ขาดคุณสมบัติที่จำเป็น' } }, editor: { title: { content: 'เนื้อหา', interaction: 'การโต้ตอบ', theme: 'รูปลักษณ์และความรู้สึก' }, field: { attribute: 'แอตทริบิวต์', badge_color: 'สีของป้าย', badge_icon: 'ไอคอนของป้าย', bar_color: 'สีแถบ', bar_effect: 'เอฟเฟกต์บนแถบ', bar_orientation: 'การวางแนวแถบ', bar_position: 'ตำแหน่งแถบ', bar_single_line: 'ข้อมูลในบรรทัดเดียว (overlay)', bar_size: 'ขนาดแถบ', center_zero: 'ศูนย์ที่กึ่งกลาง', color: 'สีหลัก', decimal: 'ทศนิยม', disable_unit: 'แสดงหน่วย', double_tap_action: 'พฤติกรรมการแตะสองครั้ง', entity: 'เอนทิตี', force_circular_background: 'บังคับพื้นหลังวงกลม', hide: 'ซ่อน', hold_action: 'พฤติกรรมการกด', icon: 'ไอคอน', icon_double_tap_action: 'พฤติกรรมการแตะไอคอนสองครั้ง', icon_hold_action: 'พฤติกรรมการกดไอคอน', icon_tap_action: 'พฤติกรรมการแตะไอคอน', layout: 'รูปแบบเนื้อหา', max_value: 'ค่าสูงสุด', max_value_attribute: 'แอตทริบิวต์ (max_value)', max_value_entity: 'ค่าสูงสุด', min_value: 'ค่าต่ำสุด', name: 'ชื่อ', percent: 'เปอร์เซ็นต์', reverse_secondary_info_row: 'แถบและข้อความสลับกัน', secondary: 'ข้อมูลรอง', state_content: 'เนื้อหาของสถานะ', tap_action: 'พฤติกรรมการแตะ', text_shadow: 'เพิ่มเงาให้ข้อความ (overlay)', theme: 'ธีม', toggle_icon: 'แสดงไอคอน', toggle_name: 'แสดงชื่อ', toggle_progress_bar: 'แสดงแถบความคืบหน้า', toggle_secondary_info: 'แสดงข้อมูลรอง', toggle_value: 'แสดงค่า', unit: 'หน่วย', use_max_entity: 'ใช้เอนทิตีสำหรับค่าสูงสุด' }, option: { bar_orientation: { ltr: 'ซ้ายไปขวา', rtl: 'ขวาไปซ้าย', up: 'ขึ้นบน (overlay)' }, bar_position: { below: 'แถบใต้เนื้อหา', bottom: 'แถบด้านล่าง (overlay)', default: 'ค่าเริ่มต้น', overlay: 'แถบซ้อนทับเนื้อหา (overlay)', top: 'แถบด้านบน (overlay)', background: 'พื้นหลังการ์ด' }, bar_size: { large: 'ใหญ่', medium: 'กลาง', small: 'เล็ก', xlarge: 'ใหญ่พิเศษ' }, layout: { horizontal: 'แนวนอน (เริ่มต้น)', vertical: 'แนวตั้ง' }, theme: { humidity: 'ความชื้น', light: 'แสง', optimal_when_high: 'เหมาะสมเมื่อสูง (แบตเตอรี่...)', optimal_when_low: 'เหมาะสมเมื่อต่ำ (CPU, RAM,...)', pm25: 'PM2.5', temperature: 'อุณหภูมิ', voc: 'VOC' } } } }, tr: { card: { msg: { appliedDefaultValue: 'Varsayılan değer otomatik olarak uygulandı.', attributeNotFound: 'Öznitelik Home Assistant\'ta bulunamadı.', discontinuousRange: 'Tanımlanan aralık süreksizdir.', entityNotFound: 'Varlık Home Assistant\'ta bulunamadı.', invalidActionObject: 'Eylem nesnesi geçersiz veya hatalı yapılandırılmış.', invalidCustomThemeArray: 'Özel tema bir dizi olmalıdır.', invalidCustomThemeEntry: 'Özel temadaki bir veya daha fazla giriş geçersiz.', invalidDecimal: 'Değer geçerli bir ondalık sayı olmalıdır.', invalidEntityId: 'Varlık kimliği geçersiz veya hatalı biçimlendirilmiş.', invalidEnumValue: 'Sağlanan değer izin verilen seçeneklerden biri değil.', invalidIconType: 'Belirtilen simge türü geçersiz veya tanınmıyor.', invalidMaxValue: 'Maksimum değer geçersiz veya sınırların üzerinde.', invalidMinValue: 'Minimum değer geçersiz veya sınırların altında.', invalidStateContent: 'Durum içeriği geçersiz veya hatalı biçimlendirilmiş.', invalidStateContentEntry: 'Durum içeriğindeki bir veya daha fazla giriş geçersiz.', invalidTheme: 'Belirtilen tema bilinmiyor. Varsayılan tema kullanılacak.', invalidTypeArray: 'Dizi türünde bir değer bekleniyordu.', invalidTypeBoolean: 'Boolean türünde bir değer bekleniyordu.', invalidTypeNumber: 'Sayı türünde bir değer bekleniyordu.', invalidTypeObject: 'Nesne türünde bir değer bekleniyordu.', invalidTypeString: 'Dize (string) türünde bir değer bekleniyordu.', invalidUnionType: 'Değer izin verilen türlerden hiçbirine uymuyor.', minGreaterThanMax: 'Minimum değer maksimum değerden büyük olamaz.', missingActionKey: 'Eylem nesnesinde gerekli bir anahtar eksik.', missingColorProperty: 'Gerekli bir renk özelliği eksik.', missingRequiredProperty: 'Gerekli bir özellik eksik.' } }, editor: { title: { content: 'İçerik', interaction: 'Etkileşimler', theme: 'Görünüm' }, field: { attribute: 'Öznitelik', badge_color: 'Rozet rengi', badge_icon: 'Rozet simgesi', bar_color: 'Çubuk rengi', bar_effect: 'Çubuk efekti', bar_orientation: 'Çubuk yönü', bar_position: 'Çubuk konumu', bar_single_line: 'Bilgiyi tek satırda göster (overlay)', bar_size: 'Çubuk boyutu', center_zero: 'Sıfırı ortala', color: 'Birincil renk', decimal: 'ondalık', disable_unit: 'Birimi göster', double_tap_action: 'Çift dokunma davranışı', entity: 'Varlık', force_circular_background: 'Dairesel arka planı zorla', hide: 'Gizle', hold_action: 'Uzun basma davranışı', icon: 'Simge', icon_double_tap_action: 'Simgeye çift dokunma davranışı', icon_hold_action: 'Simgeye uzun basma davranışı', icon_tap_action: 'Simgeye dokunma davranışı', layout: 'İçerik düzeni', max_value: 'Maksimum değer', max_value_attribute: 'Öznitelik (max_value)', max_value_entity: 'Maksimum değer', min_value: 'Minimum değer', name: 'Ad', percent: 'Yüzde', reverse_secondary_info_row: 'Çubuğu ve metni değiştir', secondary: 'İkincil bilgi', state_content: 'Durum içeriği', tap_action: 'Kısa dokunma davranışı', text_shadow: 'Metne gölge ekle (overlay)', theme: 'Tema', toggle_icon: 'Simgeyi göster', toggle_name: 'Adı göster', toggle_progress_bar: 'İlerleme çubuğunu göster', toggle_secondary_info: 'İkincil bilgiyi göster', toggle_value: 'Değeri göster', unit: 'Birim', use_max_entity: 'Maksimum değer için varlık kullan' }, option: { bar_orientation: { ltr: 'Soldan sağa', rtl: 'Sağdan sola', up: 'Yukarı (overlay)' }, bar_position: { below: 'İçeriğin altında çubuk', bottom: 'Altta çubuk (overlay)', default: 'Varsayılan', overlay: 'İçeriğin üzerine bindirme (overlay)', top: 'Üstte çubuk (overlay)', background: 'Kart arka planı' }, bar_size: { large: 'Büyük', medium: 'Orta', small: 'Küçük', xlarge: 'Çok büyük' }, layout: { horizontal: 'Yatay (varsayılan)', vertical: 'Dikey' }, theme: { humidity: 'Nem', light: 'Işık', optimal_when_high: 'Yüksekken en iyi (Pil...)', optimal_when_low: 'Düşükken en iyi (CPU, RAM...)', pm25: 'PM2.5', temperature: 'Sıcaklık', voc: 'VOC' } } } }, uk: { card: { msg: { appliedDefaultValue: 'Значення за замовчуванням було застосовано автоматично.', attributeNotFound: 'Атрибут не знайдено в HA.', discontinuousRange: 'Визначений діапазон є розривним.', entityNotFound: 'Сутність не знайдена в HA.', invalidActionObject: 'Об\'єкт дії недійсний або неправильно структурований.', invalidCustomThemeArray: 'Користувацька тема повинна бути масивом.', invalidCustomThemeEntry: 'Один або кілька записів у користувацькій темі недійсні.', invalidDecimal: 'Значення повинно бути дійсним десятковим числом.', invalidEntityId: 'ID сутності недійсний або неправильно сформований.', invalidEnumValue: 'Надане значення не є одним з дозволених варіантів.', invalidIconType: 'Зазначений тип іконки недійсний або нерозпізнаний.', invalidMaxValue: 'Максимальне значення недійсне або вище дозволених меж.', invalidMinValue: 'Мінімальне значення недійсне або нижче дозволених меж.', invalidStateContent: 'Вміст стану недійсний або неправильно сформований.', invalidStateContentEntry: 'Один або кілька записів у вмісті стану недійсні.', invalidTheme: 'Зазначена тема невідома. Буде використана тема за замовчуванням.', invalidTypeArray: 'Очікується значення типу масив.', invalidTypeBoolean: 'Очікується значення типу булевий.', invalidTypeNumber: 'Очікується значення типу число.', invalidTypeObject: 'Очікується значення типу об\'єкт.', invalidTypeString: 'Очікується значення типу рядок.', invalidUnionType: 'Значення не відповідає жодному з дозволених типів.', minGreaterThanMax: 'Мінімальне значення не може бути більшим за максимальне.', missingActionKey: 'Відсутній обов\'язковий ключ в об\'єкті дії.', missingColorProperty: 'Відсутня обов\'язкова властивість кольору.', missingRequiredProperty: 'Відсутня обов\'язкова властивість.' } }, editor: { title: { content: 'Вміст', interaction: 'Взаємодії', theme: 'Вигляд та відчуття' }, field: { attribute: 'Атрибут', badge_color: 'Колір значка', badge_icon: 'Іконка значка', bar_color: 'Колір панелі', bar_effect: 'Ефект на панелі', bar_orientation: 'Орієнтація панелі', bar_position: 'Положення панелі', bar_single_line: 'Інформація в один рядок (overlay)', bar_size: 'Розмір панелі', center_zero: 'Нуль по центру', color: 'Основний колір', decimal: 'десятковий', disable_unit: 'Показати одиницю', double_tap_action: 'Поведінка при подвійному дотику', entity: 'Сутність', force_circular_background: 'Примусовий круглий фон', hide: 'Приховати', hold_action: 'Поведінка при утриманні', icon: 'Іконка', icon_double_tap_action: 'Поведінка подвійного дотику іконки', icon_hold_action: 'Поведінка утримання іконки', icon_tap_action: 'Поведінка дотику іконки', layout: 'Розташування вмісту', max_value: 'Максимальне значення', max_value_attribute: 'Атрибут (max_value)', max_value_entity: 'Максимальне значення', min_value: 'Мінімальне значення', name: 'Назва', percent: 'Відсоток', reverse_secondary_info_row: 'Поміняти місцями панель і текст', secondary: 'Додаткова інформація', state_content: 'Вміст стану', tap_action: 'Поведінка при дотику', text_shadow: 'Додати тінь до тексту (overlay)', theme: 'Тема', toggle_icon: 'Показати іконку', toggle_name: 'Показати назву', toggle_progress_bar: 'Показати панель прогресу', toggle_secondary_info: 'Показати додаткову інформацію', toggle_value: 'Показати значення', unit: 'Одиниця', use_max_entity: 'Використовувати сутність для максимального значення' }, option: { bar_orientation: { ltr: 'Зліва направо', rtl: 'Справа наліво', up: 'Вгору (overlay)' }, bar_position: { below: 'Панель під вмістом', bottom: 'Панель знизу (overlay)', default: 'За замовчуванням', overlay: 'Панель поверх вмісту (overlay)', top: 'Панель зверху (overlay)', background: 'Фон картки' }, bar_size: { large: 'Велика', medium: 'Середня', small: 'Мала', xlarge: 'Дуже велика' }, layout: { horizontal: 'Горизонтальний (за замовчуванням)', vertical: 'Вертикальний' }, theme: { humidity: 'Вологість', light: 'Світло', optimal_when_high: 'Оптимально при високих значеннях (Батарея...)', optimal_when_low: 'Оптимально при низьких значеннях (CPU, RAM,...)', pm25: 'PM2.5', temperature: 'Температура', voc: 'VOC' } } } }, vi: { card: { msg: { appliedDefaultValue: 'Một giá trị mặc định đã được áp dụng tự động.', attributeNotFound: 'Không tìm thấy thuộc tính trong HA.', discontinuousRange: 'Phạm vi được xác định không liên tục.', entityNotFound: 'Không tìm thấy thực thể trong HA.', invalidActionObject: 'Đối tượng hành động không hợp lệ hoặc cấu trúc không đúng.', invalidCustomThemeArray: 'Chủ đề tùy chỉnh phải là một mảng.', invalidCustomThemeEntry: 'Một hoặc nhiều mục trong chủ đề tùy chỉnh không hợp lệ.', invalidDecimal: 'Giá trị phải là một số thập phân hợp lệ.', invalidEntityId: 'ID thực thể không hợp lệ hoặc không đúng định dạng.', invalidEnumValue: 'Giá trị được cung cấp không nằm trong các tùy chọn được phép.', invalidIconType: 'Loại biểu tượng được chỉ định không hợp lệ hoặc không được nhận dạng.', invalidMaxValue: 'Giá trị tối đa không hợp lệ hoặc vượt quá giới hạn cho phép.', invalidMinValue: 'Giá trị tối thiểu không hợp lệ hoặc dưới giới hạn cho phép.', invalidStateContent: 'Nội dung trạng thái không hợp lệ hoặc không đúng định dạng.', invalidStateContentEntry: 'Một hoặc nhiều mục trong nội dung trạng thái không hợp lệ.', invalidTheme: 'Chủ đề được chỉ định không xác định. Chủ đề mặc định sẽ được sử dụng.', invalidTypeArray: 'Mong đợi một giá trị kiểu mảng.', invalidTypeBoolean: 'Mong đợi một giá trị kiểu boolean.', invalidTypeNumber: 'Mong đợi một giá trị kiểu số.', invalidTypeObject: 'Mong đợi một giá trị kiểu đối tượng.', invalidTypeString: 'Mong đợi một giá trị kiểu chuỗi.', invalidUnionType: 'Giá trị không khớp với bất kỳ loại nào được phép.', minGreaterThanMax: 'Giá trị tối thiểu không thể lớn hơn giá trị tối đa.', missingActionKey: 'Một khóa bắt buộc bị thiếu trong đối tượng hành động.', missingColorProperty: 'Một thuộc tính màu bắt buộc bị thiếu.', missingRequiredProperty: 'Thuộc tính bắt buộc bị thiếu.' } }, editor: { title: { content: 'Nội dung', interaction: 'Tương tác', theme: 'Giao diện & Trải nghiệm' }, field: { attribute: 'Thuộc tính', badge_color: 'Màu huy hiệu', badge_icon: 'Biểu tượng huy hiệu', bar_color: 'Màu thanh', bar_effect: 'Hiệu ứng thanh', bar_orientation: 'Hướng thanh', bar_position: 'Vị trí thanh', bar_single_line: 'Thông tin trên một dòng (overlay)', bar_size: 'Kích thước thanh', center_zero: 'Không ở giữa', color: 'Màu chính', decimal: 'thập phân', disable_unit: 'Hiển thị đơn vị', double_tap_action: 'Hành vi chạm đôi', entity: 'Thực thể', force_circular_background: 'Buộc nền hình tròn', hide: 'Ẩn', hold_action: 'Hành vi giữ', icon: 'Biểu tượng', icon_double_tap_action: 'Hành vi chạm đôi biểu tượng', icon_hold_action: 'Hành vi giữ biểu tượng', icon_tap_action: 'Hành vi chạm biểu tượng', layout: 'Bố cục nội dung', max_value: 'Giá trị tối đa', max_value_attribute: 'Thuộc tính (max_value)', max_value_entity: 'Giá trị tối đa', min_value: 'Giá trị tối thiểu', name: 'Tên', percent: 'Phần trăm', reverse_secondary_info_row: 'Hoán đổi thanh và văn bản', secondary: 'Thông tin phụ', state_content: 'Nội dung trạng thái', tap_action: 'Hành vi chạm', text_shadow: 'Thêm bóng cho văn bản (overlay)', theme: 'Chủ đề', toggle_icon: 'Hiển thị biểu tượng', toggle_name: 'Hiển thị tên', toggle_progress_bar: 'Hiển thị thanh tiến trình', toggle_secondary_info: 'Hiển thị thông tin phụ', toggle_value: 'Hiển thị giá trị', unit: 'Đơn vị', use_max_entity: 'Sử dụng thực thể cho giá trị tối đa' }, option: { bar_orientation: { ltr: 'Trái sang phải', rtl: 'Phải sang trái', up: 'Hướng lên (overlay)' }, bar_position: { below: 'Thanh bên dưới nội dung', bottom: 'Thanh ở dưới cùng (overlay)', default: 'Mặc định', overlay: 'Thanh phủ lên nội dung (overlay)', top: 'Thanh ở trên cùng (overlay)', background: 'Nền thẻ' }, bar_size: { large: 'Lớn', medium: 'Trung bình', small: 'Nhỏ', xlarge: 'Rất lớn' }, layout: { horizontal: 'Ngang (mặc định)', vertical: 'Dọc' }, theme: { humidity: 'Độ ẩm', light: 'Ánh sáng', optimal_when_high: 'Tối ưu khi cao (Pin...)', optimal_when_low: 'Tối ưu khi thấp (CPU, RAM,...)', pm25: 'PM2.5', temperature: 'Nhiệt độ', voc: 'VOC' } } } }, 'zh-Hans': { card: { msg: { appliedDefaultValue: '默认值已自动应用。', attributeNotFound: '在 Home Assistant 中未找到属性。', discontinuousRange: '定义的范围不连续。', entityNotFound: '在 Home Assistant 中未找到实体。', invalidActionObject: '操作对象无效或结构错误。', invalidCustomThemeArray: '自定义主题必须为数组。', invalidCustomThemeEntry: '自定义主题中的一项或多项无效。', invalidDecimal: '值必须为有效的小数。', invalidEntityId: '实体 ID 无效或格式错误。', invalidEnumValue: '提供的值不在允许选项内。', invalidIconType: '指定的图标类型无效或无法识别。', invalidMaxValue: '最大值无效或超出允许范围。', invalidMinValue: '最小值无效或低于允许范围。', invalidStateContent: '状态内容无效或格式错误。', invalidStateContentEntry: '状态内容中的一项或多项无效。', invalidTheme: '指定的主题未知,将使用默认主题。', invalidTypeArray: '应为数组类型的值。', invalidTypeBoolean: '应为布尔类型的值。', invalidTypeNumber: '应为数字类型的值。', invalidTypeObject: '应为对象类型的值。', invalidTypeString: '应为字符串类型的值。', invalidUnionType: '值不符合任何允许类型。', minGreaterThanMax: '最小值不能大于最大值。', missingActionKey: '操作对象缺少必需的键。', missingColorProperty: '缺少必需的颜色属性。', missingRequiredProperty: '缺少必需的属性。' } }, editor: { title: { content: '内容', interaction: '交互', theme: '外观与体验' }, field: { attribute: '属性', badge_color: '徽章颜色', badge_icon: '徽章图标', bar_color: '进度条颜色', bar_effect: '进度条效果', bar_orientation: '进度条方向', bar_position: '进度条位置', bar_single_line: '单行信息(覆盖显示)', bar_size: '进度条大小', center_zero: '零点居中', color: '主色', decimal: '小数', disable_unit: '显示单位', double_tap_action: '双击动作', entity: '实体', force_circular_background: '强制圆形背景', hide: '隐藏', hold_action: '长按动作', icon: '图标', icon_double_tap_action: '图标双击动作', icon_hold_action: '图标长按动作', icon_tap_action: '图标点击动作', layout: '内容布局', max_value: '最大值', max_value_attribute: '属性(最大值)', max_value_entity: '使用实体的最大值', min_value: '最小值', name: '名称', percent: '百分比', reverse_secondary_info_row: '交换进度条和文本', secondary: '次要信息', state_content: '状态内容', tap_action: '点击动作', text_shadow: '添加文本阴影(overlay)', theme: '主题', toggle_icon: '显示图标', toggle_name: '显示名称', toggle_progress_bar: '显示进度条', toggle_secondary_info: '显示次要信息', toggle_value: '显示数值', unit: '单位', use_max_entity: '使用实体作为最大值' }, option: { theme: { optimal_when_low: '值低时最佳(CPU、内存等)', optimal_when_high: '值高时最佳(电池等)', light: '亮度', temperature: '温度', humidity: '湿度', pm25: 'PM2.5', voc: 'VOC' }, bar_size: { small: '小', medium: '中', large: '大', xlarge: '超大' }, bar_orientation: { ltr: '从左到右', rtl: '从右到左', up: '向上(覆盖显示)' }, bar_position: { default: '默认', below: '内容下方的进度条', top: '顶部进度条(覆盖显示)', bottom: '底部进度条(覆盖显示)', overlay: '覆盖内容的进度条', background: '卡片背景' }, layout: { horizontal: '水平(默认)', vertical: '垂直' } } } }, 'zh-Hant': { card: { msg: { appliedDefaultValue: '已自動套用預設值。', attributeNotFound: '在 Home Assistant 中找不到此屬性。', discontinuousRange: '定義的範圍不連續。', entityNotFound: '在 Home Assistant 中找不到此實體。', invalidActionObject: '動作物件無效或結構不正確。', invalidCustomThemeArray: '自訂主題必須是陣列。', invalidCustomThemeEntry: '一個或多個自訂主題項目無效。', invalidDecimal: '數值必須是正整數。', invalidEntityId: '實體 ID 無效或格式不正確。', invalidEnumValue: '提供的值不在允許的選項中。', invalidIconType: '指定的圖示類型無效或未知。', invalidMaxValue: '最大值無效或超出允許範圍。', invalidMinValue: '最小值無效或低於允許範圍。', invalidStateContent: '狀態內容無效或格式不正確。', invalidStateContentEntry: '一個或多個狀態內容項目無效。', invalidTheme: '指定的主題未知,將使用預設主題。', invalidTypeArray: '預期為陣列類型的值。', invalidTypeBoolean: '預期為布林值類型的值。', invalidTypeNumber: '預期為數字類型的值。', invalidTypeObject: '預期為物件類型的值。', invalidTypeString: '預期為字串類型的值。', invalidUnionType: '值不符合任何允許的類型。', minGreaterThanMax: '最小值不能大於最大值。', missingActionKey: '動作物件中缺少必要鍵。', missingColorProperty: '缺少必要的顏色屬性。', missingRequiredProperty: '缺少必要屬性。' } }, editor: { title: { content: '內容', interaction: '互動', theme: '外觀與主題' }, field: { attribute: '屬性', badge_color: '徽章顏色', badge_icon: '徽章圖示', bar_color: '進度條顏色', bar_effect: '進度條效果', bar_orientation: '進度條方向', bar_position: '進度條位置', bar_single_line: '單行資訊(疊加)', bar_size: '進度條大小', center_zero: '中心為零', color: '圖示顏色', decimal: '小數', disable_unit: '顯示單位', double_tap_action: '雙擊操作', entity: '實體', force_circular_background: '強制圓形背景', hide: '隱藏', hold_action: '長按操作', icon: '圖示', icon_double_tap_action: '圖示雙擊操作', icon_hold_action: '圖示長按操作', icon_tap_action: '圖示點擊操作', layout: '內容佈局', max_value: '最大值', max_value_attribute: '屬性(max_value)', max_value_entity: '最大值', min_value: '最小值', name: '名稱', percent: '百分比', reverse_secondary_info_row: '交換進度條和文字', secondary: '次要資訊', state_content: '狀態內容', tap_action: '點擊操作', text_shadow: '文字陰影(疊加)', theme: '主題', toggle_icon: '顯示圖示', toggle_name: '顯示名稱', toggle_progress_bar: '顯示進度條', toggle_secondary_info: '顯示次要資訊', toggle_value: '顯示數值', unit: '單位', use_max_entity: '使用實體作為最大值' }, option: { theme: { optimal_when_low: '數值低時最佳(CPU, RAM…)', optimal_when_high: '數值高時最佳(電池…)', light: '明亮', temperature: '溫度', humidity: '濕度', pm25: 'PM2.5', voc: 'VOC' }, bar_size: { small: '小', medium: '中', large: '大', xlarge: '特大' }, bar_orientation: { ltr: '由左到右', rtl: '由右到左', up: '向上(疊加)' }, bar_position: { default: '預設', below: '內容下方', top: '上方(疊加)', bottom: '下方(疊加)', overlay: '疊加在內容上', background: '卡片背景' }, layout: { horizontal: '水平(預設)', vertical: '垂直' } } } } }; /* eslint-enable sonarjs/no-duplicate-string */ const CARD_CSS = ` /* ============================================================================= PARAMS ============================================================================= */ :host { /* === SPACING VARIABLES === */ --spacing: 10px; --gap-entities: 16px; /* === SIZE VARIABLES === */ --shape-default-size: 36px; --icon-default-size: 24px; --entities-shape-size: 40px; --badge-size: 16px; --badge-icon-size: 12px; --badge-offset: -3px; --progress-size-xs: 6px; --progress-size-s: 8px; --progress-size-m: 12px; --progress-size-l: 16px; --progress-size-xl: 42px; --progress-size-overlay: 36px; /* === HEIGHT VARIABLES === */ --name-height: 20px; --detail-height: 16px; --entities-height: 22.4px; --entities-card-min-height: 44.8px; --vertical-name-large-height: 18px; --progress-container-height: 16px; /* === COLOR OPACITY VARIABLES === */ --shape-opacity: 20%; --hover-opacity: 4%; --active-opacity: 15%; --icon-hover-opacity: 40%; --card-hover-mix: 96%; --card-active-mix: 85%; /* === TRANSITION VARIABLES === */ --progress-transition: 0.3s ease; /* === TYPOGRAPHY VARIABLES === */ --name-letter-spacing: 0.1px; --detail-letter-spacing: 0.4px; /* === LAYOUT VARIABLES === */ --vertical-gap: 1px; /* === HA RIPPLE === */ --ha-ripple-hover-opacity: 0.04; --ha-ripple-pressed-opacity: 0.12; /* === BORDER RADIUS === */ --ha-standard-border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg)); } .${CARD.style.bar.sizeOptions.small.label} { --progress-size: var(--epb-progress-bar-size, var(--progress-size-s)); } .${CARD.style.bar.sizeOptions.medium.label} { --progress-size: var(--epb-progress-bar-size, var(--progress-size-m)); } .${CARD.style.bar.sizeOptions.large.label} { --progress-size: var(--epb-progress-bar-size, var(--progress-size-l)); } .${CARD.style.bar.sizeOptions.xlarge.label} { --progress-size: var(--epb-progress-bar-size, var(--progress-size-xl)); --progress-container-height: var(--progress-size-xl); } ha-card.overlay { --progress-size: var(--epb-progress-bar-size, var(--progress-size-overlay)); --progress-container-height: var(--epb-progress-bar-size, var(--progress-size-overlay)); } .bottom-container, .top-container { --progress-size: var(--epb-progress-bar-size, var(--progress-size-xs)); --progress-container-height: var(--progress-size-xs); } /* ============================================================================= BASE CARD ============================================================================= */ ${CARD.htmlStructure.card.element} { --ha-ripple-color: var(--epb-icon-and-shape-color, var(--icon-and-shape-color, var(--state-icon-color))); --current-card-min-width: var(${CARD.style.dynamic.card.minWidth.var}, 100%); --current-card-min-height: 0; --current-card-height: var(${CARD.style.dynamic.card.height.var}, 100%); --current-card-padding: 0 var(--spacing); --current-card-margin: 0 auto; --current-card-border-radius: var(--ha-standard-border-radius); display: flex; align-items: center; justify-content: center; position: relative; /* permet top/bottom */ margin: var(--current-card-margin); padding: var(--current-card-padding); min-width: var(--epb-card-width, var(--current-card-min-width)); width: var(--epb-card-width, auto); min-height: var(--current-card-min-height); height: var(--epb-card-height, var(--current-card-height)); border-radius: var(--epb-card-border-radius, var(--current-card-border-radius)); border-width: var(--epb-card-border-width, var(--ha-card-border-width, 1px)); border-color: var(--epb-card-border-color, var(--ha-card-border-color, var(--divider-color, #e0e0e0))); border-style: var(--epb-card-border-style, solid); overflow: hidden; font-family: var(--epb-card-font-family, var(--ha-font-family-body)); -moz-osx-font-smoothing: var(--ha-font-smoothing); -webkit-font-smoothing: antialiased; transition-property: background-color, box-shadow, border-color; } .horizontal { --current-card-min-height: var(${CARD.style.dynamic.card.height.var}, ${CARD.layout.orientations.horizontal.minHeight}); --current-card-padding: 0 var(--spacing); } .vertical { --current-card-min-height: var(${CARD.style.dynamic.card.height.var}, ${CARD.layout.orientations.vertical.minHeight}); --current-card-padding: var(--spacing); } .marginless { --current-card-min-height: unset; --current-card-padding: 0; --current-card-margin: 0; } /* === BADGE === */ .progress-badge { --current-card-height: var(--ha-badge-size, 36px); --current-card-min-height: var(--ha-badge-size, 36px); --current-card-min-width: var(--card-min-width, var(--ha-badge-size, 130px)); --current-card-border-radius: var(--ha-badge-border-radius,calc(var(--ha-badge-size,36px)/ 2)); } /* === TYPE: PICTURE-ELEMENTS === */ .type-picture-elements { --current-card-min-width: var(${CARD.style.dynamic.card.minWidth.var}, 200px); } /* === FRAMELESS & ENTITIES STYLES === */ .type-entities, .type-custom-vertical-stack-in-card, .${CARD.style.dynamic.frameless.class} { --ha-card-background: transparent; --ha-card-border-width: 0; --ha-card-box-shadow: none; } .type-entities { --current-card-padding: 0; --current-card-margin: 0; --ha-ripple-hover-opacity: 0; --ha-ripple-pressed-opacity: 0; --current-card-height: var(--entities-card-min-height); /* 44.8 px*/ --current-card-min-height: var(--entities-card-min-height); transition: none !important; } /* ============================================================================= MAIN CONTAINER ============================================================================= */ .${CARD.htmlStructure.sections.container.class} { display: flex; flex-direction: var(--current-container-flex-direction, row); align-items: center; justify-content: center; gap: var(--current-container-gap, var(--spacing)); width: 100%; height: 100%; overflow: var(--current-container-overflow, visible); padding-top: var(--current-container-padding-top, 0); box-sizing: var(--current-container-box-sizing, content-box); flex-wrap: var(--current-container-flex-wrap, nowrap); } .horizontal { --current-container-flex-direction: row; --current-container-padding-top: 0; --current-container-min-height: var(${CARD.style.dynamic.card.height.var}, ${CARD.layout.orientations.horizontal.minHeight}); --current-container-overflow: visible; --current-container-gap: var(--spacing); --current-container-box-sizing: content-box; --current-container-flex-wrap: wrap; } .vertical { --current-container-flex-direction: column; --current-container-min-height: var(${CARD.style.dynamic.card.height.var}, ${CARD.layout.orientations.vertical.minHeight}); --current-container-overflow: hidden; --current-container-gap: var(--spacing); --current-container-box-sizing: border-box; --current-container-flex-wrap: nowrap; } .vertical.default { --current-container-padding-top: var(--progress-size); } .${CARD.htmlStructure.sections.container.class}.vertical.up-orientation.overlay { --current-container-gap: 9.5px; } .type-entities .${CARD.htmlStructure.sections.container.class} { --current-container-gap: var(--gap-entities); --current-container-min-height: var(--entities-card-min-height); } .${CARD.style.dynamic.marginless.class} .${CARD.htmlStructure.sections.container.class} { --current-container-min-height: 0; --current-container-padding-top: 0; } /* ============================================================================= TOP, BELOW & BOTTOM ============================================================================= */ ha-card:is(.bottom, .top, .below) { --group-max-width: 100%; --group-width: 100%; } ha-card.below { --current-card-padding: var(--spacing); flex-direction: column; flex-wrap: nowrap; align-items: stretch; justify-content: space-between; gap: var(--spacing); } ha-card.below > .container { flex: 1 1 auto; min-height: 0; } ha-card.vertical.xlarge.below .container { --current-container-padding-top: 0; } ha-card.below .${CARD.htmlStructure.elements.progressBar.container.class} { --current-progress-container-height: var(--progress-size); } ha-card.vertical.xlarge.below .${CARD.htmlStructure.elements.progressBar.container.class} { margin: 0; } .below-container { width: 100%; display: flex; overflow: hidden; height: var(--progress-size); flex-shrink: 0; } .horizontal.xlarge .container { align-content: center; } .bottom-container, .top-container { position: absolute; width: 100%; left: 0; } .top-container { top: 0; } .bottom-container { bottom: 0; } .bottom-container .bar-container, .top-container .bar-container { height: var(--progress-size); } ha-card.background { --progress-size: 100%; --progress-container-height: 100%; } .background-container { position: absolute; inset: 0; border-radius: inherit; overflow: hidden; z-index: 0; } .background .${CARD.htmlStructure.sections.container.class} { position: relative; z-index: 1; } :is(.background-container) :is(.${CARD.htmlStructure.elements.progressBar.bar.class}, .${CARD.htmlStructure.elements.progressBar.inner.class}) { --bar-radius: 0; --inner-radius: 0; } /* ============================================================================= TREND ============================================================================= */ .trend-indicator, .trend-icon { display: flex; align-items: center; justify-content: center; width: var(--badge-size); height: var(--badge-size); } .trend-indicator { position: absolute; top: 2px; right: 2px; } .trend-icon { color: var(--state-icon-color); } /* ============================================================================= ICON SECTION (ICON, SHAPE, BADGE) ============================================================================= */ .${CARD.htmlStructure.sections.icon.class} { --current-shape-size: var(--shape-default-size); display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; width: var(--current-shape-size); height: var(--current-shape-size); flex-shrink: 0; } .type-entities .${CARD.htmlStructure.sections.icon.class} { --current-shape-size: var(--entities-shape-size); } .${CARD.layout.orientations.vertical.label}.${CARD.style.dynamic.marginless.class} .${CARD.htmlStructure.sections.icon.class} { margin-top: unset !important; } /* === SHAPE & ICON === */ .${CARD.htmlStructure.elements.shape.class} { --current-shape-background-color: color-mix(in srgb, var(--epb-icon-and-shape-color, var(${CARD.style.dynamic.iconAndShape.color.var}, ${CARD.style.dynamic.iconAndShape.color.default})) var(--shape-opacity), transparent); --ha-ripple-hover-opacity: 0.15; position: relative; display: flex; align-items: center; justify-content: center; width: var(--current-shape-size); height: var(--current-shape-size); border-radius: 50%; background-color: var(--current-shape-background-color); } .type-entities .${CARD.htmlStructure.elements.shape.class} { --ha-ripple-hover-opacity: 0; --ha-ripple-pressed-opacity: 0; --current-shape-background-color: transparent; } .${CARD.htmlStructure.elements.icon.class}, .custom-icon-img { --current-icon-size: var(--icon-default-size); display: flex; align-items: center; justify-content: center; width: var(--current-icon-size); height: var(--current-icon-size); } .progress-badge .${CARD.htmlStructure.sections.icon.class}, .progress-badge .${CARD.htmlStructure.elements.icon.class}, .progress-badge .${CARD.htmlStructure.elements.shape.class}, .progress-badge .custom-icon-img { --current-icon-size: 18px; --current-shape-size: 18px; } .progress-badge .icon ha-state-icon { --current-icon-size: 18px; --mdc-icon-size: var(--current-icon-size); --ha-icon-display: flex; height: var(--current-icon-size); width: var(--current-icon-size); display: flex; align-items: center; justify-content: center; } .${CARD.htmlStructure.elements.icon.class} { color: var(--epb-icon-and-shape-color, var(${CARD.style.dynamic.iconAndShape.color.var}, ${CARD.style.dynamic.iconAndShape.color.default})); } .custom-icon-img { border-radius: 50%; object-fit: cover; } /* ============================================================================= CONTENT SECTION (TEXT CONTENT) ============================================================================= */ .${CARD.htmlStructure.sections.content.class} { --current-content-height: calc(var(--name-height) + var(--detail-height)); display: flex; flex-direction: column; justify-content: center; flex-grow: var(--current-content-flex-grow); flex-shrink: 1; width: var(--current-content-width); height: var(--current-content-height); gap: var(--current-content-gap, 0); min-width: 0; overflow: hidden; position: relative; /* overlay */ } ha-card.horizontal .${CARD.htmlStructure.sections.content.class} { --current-content-width: calc(100% - 56px); --current-content-flex-grow: 1; --current-content-gap: 0; } ha-card.vertical .${CARD.htmlStructure.sections.content.class} { --current-content-width: 100%; --current-content-flex-grow: 0; --current-content-gap: var(--vertical-gap); } ha-card.vertical.default .${CARD.htmlStructure.sections.content.class} { --current-content-height: calc(var(--name-height) + var(--detail-height) + var(--progress-size)); } ha-card.type-entities .${CARD.htmlStructure.sections.content.class} { --current-content-height: unset; } .progress-badge .${CARD.htmlStructure.sections.content.class} { --current-content-height: unset; } .overlay .${CARD.htmlStructure.sections.content.class} { --current-content-height: var(--progress-size); } .vertical.up-orientation.overlay .${CARD.htmlStructure.sections.content.class} { --current-content-flex-grow: 1; --current-content-width: var(--epb-progress-bar-size, 50%); --current-content-height: 100%; } /* === TEXT ELEMENTS === */ .${CARD.htmlStructure.elements.nameContent.class}, .${CARD.htmlStructure.elements.secondaryInfoWrapper.class} { /* flex layout, dimensions, overflow, alignement*/ display: flex; z-index: 1; align-items: var(--group-align-items, center); justify-content: var(--group-justify-content, flex-start); flex-grow: var(--group-flex-grow, initial); width: var(--group-width, auto); min-width: var(--group-min-width, 0); max-width: var(--group-max-width, none); height: var(--group-height); line-height: var(--group-height); /*fix size*/ overflow: var(--group-overflow, hidden); text-align: var(--group-text-align, left); box-sizing: var(--group-box-sizing, content-box); margin-left: var(--group-margin-left); } .${CARD.htmlStructure.elements.nameContent.class} { --group-height: var(--name-height); } .${CARD.htmlStructure.elements.secondaryInfoWrapper.class} { --group-height: var(--detail-height); --group-min-width: 45px; --group-max-width: 60%; } .progress-badge .${CARD.htmlStructure.elements.nameContent.class} { --group-height: var(--ha-font-size-xs); } .progress-badge .${CARD.htmlStructure.elements.secondaryInfoWrapper.class} { --group-min-width: unset; --group-max-width: unset; } ha-card:is(.vertical, .xlarge, .bottom, .top) .${CARD.htmlStructure.elements.secondaryInfoWrapper.class} { --group-min-width: 100%; --group-max-width: 100%; } .row-reverse .${CARD.htmlStructure.elements.secondaryInfoWrapper.class} { --group-min-width: unset; } .${CARD.layout.orientations.vertical.label} { --group-justify-content: center; --group-width: 100%; --group-max-width: 100%; --group-flex-grow: 0; --group-text-align: center; --group-box-sizing: border-box; } .${CARD.layout.orientations.vertical.label} .${CARD.style.bar.sizeOptions.large.label} { --name-height: var(--vertical-name-large-height); } .overlay :is(.${CARD.htmlStructure.elements.nameContent.class}, .${CARD.htmlStructure.elements.secondaryInfoWrapper.class}) { --group-margin-left: 7px; } .vertical.up-orientation.overlay :is(.${CARD.htmlStructure.elements.nameContent.class}, .${CARD.htmlStructure.elements.secondaryInfoWrapper.class}) { --group-margin-left: 0; } .ellipsis-wrapper { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; line-height: 100%; width: 100%; } .ellipsis-wrapper span { display: inline; } .${CARD.htmlStructure.elements.nameValue.class}, .${CARD.htmlStructure.elements.secondaryInfoValue.class} { color: var(--text-color); font-size: var(--text-font-size); font-weight: var(--text-font-weight); height: var(--text-height); line-height: var(--text-line-height); letter-spacing: var(--text-letter-spacing); margin-right: var(--text-margin-right); text-shadow: var(--text-shadow); } .${CARD.htmlStructure.elements.nameValue.class} { --text-color: var(--epb-name-color, var(--primary-text-color)); --text-font-size: var(--epb-name-font-size, var(--ha-font-size-m)); --text-font-weight: var(--epb-name-font-weight, var(--ha-font-weight-medium)); --text-height: var(--name-height); --text-line-height: var(--name-height); --text-letter-spacing: var(--epb-name-letter-spacing, var(--name-letter-spacing)); --text-margin-right: 0; } .${CARD.htmlStructure.elements.secondaryInfoValue.class} { --text-color: var(--epb-name-color, var(--primary-text-color)); --text-font-size: var(--ha-font-size-s); --text-font-weight: var(--ha-font-weight-body); --text-height: var(--detail-height); --text-line-height: var(--detail-height); --text-letter-spacing: var(--epb-detail-letter-spacing, var(--detail-letter-spacing)); --text-margin-right: 0; } .progress-badge .${CARD.htmlStructure.elements.nameValue.class} { --text-color: var(--epb-name-color, var(--secondary-text-color)); --text-font-size: var(--ha-font-size-xs); --text-font-weight: var(--ha-font-weight-medium); --text-height: 10px; --text-line-height: 10px; --text-margin-right: 5px; --text-letter-spacing: var(--name-letter-spacing); } .progress-badge .${CARD.htmlStructure.elements.secondaryInfoValue.class} { --text-color: var(--primary-text-color); --text-font-size: var(--ha-badge-font-size, var(--ha-font-size-s)); --text-font-weight: var(--ha-font-weight-medium); --text-height: var(--text-font-size); --text-line-height: var(--ha-line-height-condensed); --text-letter-spacing: var(--name-letter-spacing); } .type-entities :is(.${CARD.htmlStructure.elements.nameValue.class}, .${CARD.htmlStructure.elements.secondaryInfoValue.class}) { --text-height: var(--entities-height); --text-font-weight: var(--ha-font-weight-normal); --text-line-height: var(--ha-line-height-normal); } .type-entities .${CARD.htmlStructure.elements.secondaryInfoValue.class} { --text-color: var(--secondary-text-color); --text-font-size: var(--ha-font-size-m); } .overlay.text-shadow :is(.${CARD.htmlStructure.elements.nameValue.class}, .${CARD.htmlStructure.elements.secondaryInfoValue.class}) { --text-shadow: 1px 1px 2px var(--card-background-color); } /* === SECONDARY INFO === */ .${CARD.htmlStructure.elements.secondaryInfo.class} { display: flex; flex-direction: var(--current-secondary-info-flex-direction); align-items: var(--current-secondary-info-align-items); gap: var(--current-secondary-info-gap, var(--spacing)); width: var(--current-secondary-info-width, auto); min-width: var(--current-secondary-info-min-width, auto); justify-content: space-between; } .secondary-info-wrapper:has(.secondary-info-extra:empty):has(.secondary-info-main:empty) { display: none; } .${CARD.layout.orientations.horizontal.label} { --current-secondary-info-flex-direction: var(--secondary-info-row-reverse, row); --current-secondary-info-align-items: stretch; --current-secondary-info-gap: var(--spacing); --current-secondary-info-width: auto; --current-secondary-info-min-width: auto; } .${CARD.layout.orientations.vertical.label} { --current-secondary-info-flex-direction: column; --current-secondary-info-align-items: center; --current-secondary-info-gap: unset; --current-secondary-info-width: 100%; --current-secondary-info-min-width: 0; } .progress-badge { --current-secondary-info-gap: 5px; } .multiline { display: inline-block; height: 16px; line-height: 0.95; font-size: 8px; margin: 0; padding: 0; } .info-multiline .secondary-info, .info-multiline .secondary-info * { height: 18px; font-size: 9px; } .vertical.info-multiline :is( .secondary-info, .secondary-info .secondary-info-wrapper, .secondary-info .secondary-info-extra ) { height: unset !important; } .vertical.info-multiline .secondary-info .bar-container { height: 16px; } /* ============================================================================= PROGRESS BAR ============================================================================= */ /* ==== CONTAINER === */ .${CARD.htmlStructure.elements.progressBar.container.class} { display: flex; justify-content: center; align-items: center; flex-grow: 1; height: var(--type-entities-combined-line-height, var(--current-progress-container-height)); } .overlay .${CARD.htmlStructure.elements.progressBar.container.class} { position: absolute; width: 100%; height: 100%; } .${CARD.layout.orientations.horizontal.label}.${CARD.style.bar.sizeOptions.small.label} .${CARD.htmlStructure.elements.progressBar.container.class}, .${CARD.layout.orientations.horizontal.label}.${CARD.style.bar.sizeOptions.medium.label} .${CARD.htmlStructure.elements.progressBar.container.class}, .${CARD.layout.orientations.horizontal.label}.${CARD.style.bar.sizeOptions.large.label} .${CARD.htmlStructure.elements.progressBar.container.class} { max-width: var(--progress-bar-max-width, unset); } .horizontal { --current-progress-container-height: var(--progress-container-height); } .vertical { --current-progress-container-height: var(--progress-size); } .vertical.xlarge .bar-container { margin-top: 23px; } /* ==== BAR === */ .${CARD.htmlStructure.elements.progressBar.bar.class} { --bar-radius: var(--ha-standard-border-radius); position: relative; height: var(--bar-height, var(--progress-size, 100%)); max-height: var(--bar-max-height, var(--progress-size, 100%)); width: 100%; flex-grow: var(--bar-flex-grow); overflow: hidden; background-color: var(${CARD.style.dynamic.progressBar.background.var}, var(--divider-color)); border-radius: var(--epb-progress-bar-radius, var(--bar-radius)); } .${CARD.layout.orientations.vertical.label} .${CARD.htmlStructure.elements.progressBar.bar.class} { --bar-flex-grow: 0; } .overlay .${CARD.layout.orientations.vertical.label} .${CARD.htmlStructure.elements.progressBar.bar.class} { --bar-height: 100%; --bar-max-height: 100%; } /* ==== INNER === */ /* --- Base ---*/ .${CARD.htmlStructure.elements.progressBar.inner.class} { --inner-radius: 0; /* radius value */ --_r: var(--epb-progress-inner-radius, var(--inner-radius)); /* user choice Vs system value */ --inner-border-radius: var(--_r); /* schema */ position: absolute; inset: var(--inner-inset, 0 0 0 0); background: var(--inner-background); border-radius: var(--inner-border-radius); transform-origin: var(--inner-transform-origin, left); transform: scaleX(var(--scale-x,0)) scaleY(var(--scale-y,0)); will-change: transform; backface-visibility: hidden; contain: layout paint; } /* --- Animation ---*/ .horizontal-bar .${CARD.htmlStructure.elements.progressBar.inner.class} { --scale-x: 0; --scale-y: 1; } .horizontal-bar.transition-ready .${CARD.htmlStructure.elements.progressBar.inner.class} { --scale-x: var(--inner-size); transition: transform var(--progress-transition); } .vertical-bar .${CARD.htmlStructure.elements.progressBar.inner.class} { --scale-x: 1; --scale-y: 0; } .vertical-bar.transition-ready .${CARD.htmlStructure.elements.progressBar.inner.class} { --scale-y: var(--inner-size); transition: transform var(--progress-transition); } /* center zero - positiveInner */ .center-zero.horizontal-bar .${CARD.htmlStructure.elements.progressBar.inner.class}.positive { --inner-inset: 0 0 0 50%; --inner-border-radius: 0 var(--_r) var(--_r) 0; } /* center zero - negativeInner */ .center-zero.horizontal-bar .${CARD.htmlStructure.elements.progressBar.inner.class}.negative { --inner-inset: 0 50% 0 0; --inner-border-radius: var(--_r) 0 0 var(--_r); --inner-transform-origin: right; } /* --- Vertical --- */ .vertical-bar .${CARD.htmlStructure.elements.progressBar.inner.class}.positive { --inner-transform-origin: bottom; --inner-border-radius: var(--_r) var(--_r) 0 0; } .vertical-bar.center-zero .${CARD.htmlStructure.elements.progressBar.inner.class}.positive { --inner-inset: 0 0 50% 0; } .vertical-bar.center-zero .${CARD.htmlStructure.elements.progressBar.inner.class}.negative { --inner-inset: 50% 0 0 0; --inner-transform-origin: top; --inner-border-radius: 0 0 var(--_r) var(--_r); } /* --- inner size/background --- */ .${CARD.htmlStructure.elements.progressBar.inner.class}.positive { --inner-size: var(${CARD.style.dynamic.progressBar.value.var}, 0); --inner-background: var(--epb-progress-bar-color, var(--progress-effect, var(${CARD.style.dynamic.progressBar.color.var}, ${CARD.style.dynamic.progressBar.color.default}))); } .center-zero .${CARD.htmlStructure.elements.progressBar.inner.class}.negative { --inner-size: calc(var(${CARD.style.dynamic.progressBar.value.var}, 0) * -1); --inner-background: var(--epb-progress-bar-color, var(--progress-effect-neg, var(${CARD.style.dynamic.progressBar.color.var}, ${CARD.style.dynamic.progressBar.color.default}))); } /* === ORIENTATION === */ .${CARD.style.dynamic.progressBar.orientation.rtl} .${CARD.htmlStructure.elements.progressBar.bar.class} { transform: scaleX(-1); } /* === RADIUS EFFECT === */ /* positiveInner / negativeInner */ .entity-progress-feature :is(.${CARD.htmlStructure.elements.progressBar.bar.class}, .${CARD.htmlStructure.elements.progressBar.inner.class}) { --bar-radius: var(--feature-border-radius); --inner-radius: var(--feature-border-radius); } /* positiveInner / negativeInner */ :is(.top-container, .bottom-container) :is(.${CARD.htmlStructure.elements.progressBar.bar.class}, .${CARD.htmlStructure.elements.progressBar.inner.class}) { --bar-radius: 0; --inner-radius: 0; } /* positiveInner / negativeInner */ .${CARD.style.dynamic.progressBar.effect.radius.class} :is(.${CARD.htmlStructure.elements.progressBar.inner.class}) { --inner-radius: var(--ha-standard-border-radius); } /* === VARIANTS === */ /* ----- glass ----- */ .${CARD.style.dynamic.progressBar.effect.glass.class} { --progress-effect: linear-gradient(90deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1)); --progress-effect-neg: linear-gradient(270deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1)); } .vertical.up-orientation.${CARD.style.dynamic.progressBar.effect.glass.class} { --progress-effect: linear-gradient(0deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1)); --progress-effect-neg: linear-gradient(180deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1)); } /* ----- gradient / gradient-reverse ----- */ .${CARD.style.dynamic.progressBar.effect.gradient.class}, .${CARD.style.dynamic.progressBar.effect.gradientReverse.class} { --progress-effect-gradient: linear-gradient( 90deg, color-mix(in srgb, white 40%, var(${CARD.style.dynamic.progressBar.color.var}, ${CARD.style.dynamic.progressBar.color.default})), var(${CARD.style.dynamic.progressBar.color.var}, ${CARD.style.dynamic.progressBar.color.default}) ); --progress-effect-gradient-rev: linear-gradient( 270deg, color-mix(in srgb, white 40%, var(${CARD.style.dynamic.progressBar.color.var}, ${CARD.style.dynamic.progressBar.color.default})), var(${CARD.style.dynamic.progressBar.color.var}, ${CARD.style.dynamic.progressBar.color.default}) ); } .vertical.up-orientation.${CARD.style.dynamic.progressBar.effect.gradient.class}, .vertical.up-orientation.${CARD.style.dynamic.progressBar.effect.gradientReverse.class} { --progress-effect-gradient: linear-gradient( 0deg, color-mix(in srgb, white 40%, var(--progress-bar-color, var(--state-icon-color))), var(--progress-bar-color, var(--state-icon-color)) ); --progress-effect-gradient-rev: linear-gradient( 180deg, color-mix(in srgb, white 40%, var(--progress-bar-color, var(--state-icon-color))), var(--progress-bar-color, var(--state-icon-color)) ); } .${CARD.style.dynamic.progressBar.effect.gradient.class} { --progress-effect: var(--progress-effect-gradient); --progress-effect-neg: var(--progress-effect-gradient-rev); } .${CARD.style.dynamic.progressBar.effect.gradientReverse.class} { --progress-effect: var(--progress-effect-gradient-rev); --progress-effect-neg: var(--progress-effect-gradient); } /* ----- shimmer / shimmer-reverse ----- */ .${CARD.style.dynamic.progressBar.effect.shimmer.class} .${CARD.htmlStructure.elements.progressBar.inner.class}, .${CARD.style.dynamic.progressBar.effect.shimmerReverse.class} .${CARD.htmlStructure.elements.progressBar.inner.class} { overflow: hidden; position: absolute; } .${CARD.style.dynamic.progressBar.effect.shimmer.class} .${CARD.htmlStructure.elements.progressBar.inner.class}::after, .${CARD.style.dynamic.progressBar.effect.shimmerReverse.class} .${CARD.htmlStructure.elements.progressBar.inner.class}::after { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(var(--shimmer-direction, 90deg), transparent, rgba(255, 255, 255, 0.4), transparent); animation: var(--shimmer-animation) 2s infinite; } /* horizontales */ .${CARD.style.dynamic.progressBar.effect.shimmer.class} { --shimmer-direction: 90deg; --shimmer-animation: shimmer-ltr; } .${CARD.style.dynamic.progressBar.effect.shimmerReverse.class} { --shimmer-direction: 90deg; --shimmer-animation: shimmer-rtl; } /* verticales */ .vertical.up-orientation.${CARD.style.dynamic.progressBar.effect.shimmer.class} { --shimmer-direction: 0deg; --shimmer-animation: shimmer-btt; } .vertical.up-orientation.${CARD.style.dynamic.progressBar.effect.shimmerReverse.class} { --shimmer-direction: 0deg; --shimmer-animation: shimmer-ttb; } @keyframes shimmer-ltr { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } @keyframes shimmer-rtl { 0% { transform: translateX(100%); } 100% { transform: translateX(-100%); } } @keyframes shimmer-btt { 0% { transform: translateY(100%); } 100% { transform: translateY(-100%); } } @keyframes shimmer-ttb { 0% { transform: translateY(-100%); } 100% { transform: translateY(100%); } } /* ============================================================================= WATERMARKS ============================================================================= */ /* low, high, center */ .mark { display: var(--mark-display, none); position: absolute; box-sizing: border-box; opacity: var(--epb-watermark-opacity, var(--watermark-opacity-value, 0.8)); top: var(--mark-top, 0); /* Horizontal */ bottom: var(--mark-bottom, auto); left: var(--mark-left, auto); right: var(--mark-right, auto); width: var(--mark-width, 100%); height: var(--mark-height, 100%); background: var(--mark-background); } .vertical.up-orientation.overlay .mark { --mark-top: auto; --mark-bottom: 0; --mark-left: 0; --mark-width: 100%; } /* --- ZERO MARK -- */ .${CARD.htmlStructure.elements.progressBar.zeroMark.class} { --mark-display: flex; --mark-width: var(--epb-zero-mark-width, 1px); --mark-left: 50%; --mark-background: var(--epb-zero-mark-color, white); } .vertical.up-orientation.overlay .${CARD.htmlStructure.elements.progressBar.zeroMark.class} { --mark-height: var(--epb-zero-mark-width, 1px); --mark-top: 50%; } /* --- Base watermark styles ---*/ .watermark { --wm-line-size: var(--epb-watermark-line-size, var(--watermark-line-size, 1px)); --wm-circle-size: var(--watermark-circle-size, 5px); --wm-tri-size: var(--watermark-triangle-size, 8px); --wm-half-line: calc(var(--wm-line-size) /2); --wm-half-circle: calc(var(--wm-circle-size) / 2); --wm-half-tri: calc(var(--wm-tri-size) / 2); } .${CARD.htmlStructure.elements.progressBar.lowWatermark.class} { --wm-value: var(--low-watermark-value, 20%); --wm-color: var(--epb-low-watermark-color, var(--low-watermark-color, var(--red-color))); } .${CARD.htmlStructure.elements.progressBar.highWatermark.class} { --wm-value: var(--high-watermark-value, 80%); --wm-color: var(--epb-high-watermark-color, var(--high-watermark-color, var(--red-color))); } :is(.lwm-area, .lwm-blended, .lwm-line, .lwm-round) .${CARD.htmlStructure.elements.progressBar.lowWatermark.class}, :is(.hwm-area, .hwm-blended, .hwm-line, .hwm-round) .${CARD.htmlStructure.elements.progressBar.highWatermark.class} { --mark-background: var(--wm-color); } /* ---------- show ---------- */ .show-lwm .${CARD.htmlStructure.elements.progressBar.lowWatermark.class}, .show-hwm .${CARD.htmlStructure.elements.progressBar.highWatermark.class} { --mark-display: flex; } /* ---------- Area, Blended, Striped positioning ---------- */ :is(.lwm-area, .lwm-blended, .lwm-striped) .${CARD.htmlStructure.elements.progressBar.lowWatermark.class} { --mark-left: 0; --mark-width: var(--wm-value); } :is(.hwm-area, .hwm-blended, .hwm-striped) .${CARD.htmlStructure.elements.progressBar.highWatermark.class} { --mark-right: 0; --mark-width: calc(100% - var(--wm-value)); } .vertical.up-orientation.overlay:is(.lwm-area, .lwm-blended, .lwm-striped) .${CARD.htmlStructure.elements.progressBar.lowWatermark.class} { --mark-height: var(--wm-value); } .vertical.up-orientation.overlay:is(.hwm-area, .hwm-blended, .hwm-striped) .${CARD.htmlStructure.elements.progressBar.highWatermark.class} { --mark-bottom: var(--wm-value); --mark-height: calc(100% - var(--wm-value)); } /* ---------- Blended ---------- */ .lwm-blended .${CARD.htmlStructure.elements.progressBar.lowWatermark.class}, .hwm-blended .${CARD.htmlStructure.elements.progressBar.highWatermark.class} { mix-blend-mode: hard-light; } /* ---------- Striped ---------- */ .lwm-striped .${CARD.htmlStructure.elements.progressBar.lowWatermark.class}, .hwm-striped .${CARD.htmlStructure.elements.progressBar.highWatermark.class} { --mark-background: repeating-linear-gradient(-45deg, var(--wm-color) 0, var(--wm-color) 3px, transparent 3px, transparent 6px); } /* ---------- Line ---------- */ .lwm-line .${CARD.htmlStructure.elements.progressBar.lowWatermark.class}, .hwm-line .${CARD.htmlStructure.elements.progressBar.highWatermark.class} { --wm-position: calc(var(--wm-value) - var(--wm-half-line)); --mark-width: var(--wm-line-size); --mark-left: var(--wm-position); border: none; transform: none; } .vertical.up-orientation.overlay.lwm-line .${CARD.htmlStructure.elements.progressBar.lowWatermark.class}, .vertical.up-orientation.overlay.hwm-line .${CARD.htmlStructure.elements.progressBar.highWatermark.class} { --mark-height: var(--wm-line-size); --mark-bottom: var(--wm-position); } /* ---------- Round ---------- */ .lwm-round .${CARD.htmlStructure.elements.progressBar.lowWatermark.class}, .hwm-round .${CARD.htmlStructure.elements.progressBar.highWatermark.class} { --mark-top: 50%; --mark-width: var(--wm-circle-size); --mark-height: var(--wm-circle-size); transform: translateY(-50%); border-radius: 50%; border: none; } .lwm-round .${CARD.htmlStructure.elements.progressBar.lowWatermark.class} { --mark-left: calc(var(--wm-value) - var(--wm-half-circle)); } .hwm-round .${CARD.htmlStructure.elements.progressBar.highWatermark.class} { --mark-left: calc(var(--wm-value) - var(--wm-half-circle)); } .vertical.up-orientation.overlay.lwm-round .${CARD.htmlStructure.elements.progressBar.lowWatermark.class}, .vertical.up-orientation.overlay.hwm-round .${CARD.htmlStructure.elements.progressBar.highWatermark.class} { --mark-left: 50%; --mark-right: auto; --mark-top: auto; --mark-bottom: calc(var(--wm-value) - var(--wm-half-circle)); --mark-width: var(--wm-circle-size); transform: translateX(-50%); } /* ---------- Triangle ---------- */ .lwm-triangle .${CARD.htmlStructure.elements.progressBar.lowWatermark.class}, .hwm-triangle .${CARD.htmlStructure.elements.progressBar.highWatermark.class} { --mark-left: calc(var(--wm-value) - var(--wm-half-tri)); --mark-width: 0; --mark-height: 0; --mark-background: transparent; border-top: var(--wm-tri-size) solid var(--wm-color); border-left: var(--wm-half-tri) solid transparent; border-right: var(--wm-half-tri) solid transparent; } .vertical.up-orientation.overlay.lwm-triangle .${CARD.htmlStructure.elements.progressBar.lowWatermark.class}, .vertical.up-orientation.overlay.hwm-triangle .${CARD.htmlStructure.elements.progressBar.highWatermark.class} { --mark-left: 0; --mark-bottom: calc(var(--wm-value) - var(--wm-half-tri)); border-right: none; border-top: var(--wm-half-tri) solid transparent; border-left: var(--wm-tri-size) solid var(--wm-color); border-bottom: var(--wm-half-tri) solid transparent; } /* ============================================================================= BADGE ============================================================================= */ .${CARD.htmlStructure.elements.badge.container.class} { display: none; align-items: center; justify-content: center; position: absolute; z-index: 2; top: var(--badge-offset); right: var(--badge-offset); inset-inline-end: var(--badge-offset); inset-inline-start: initial; width: var(--badge-size); height: var(--badge-size); border-radius: 50%; background-color: var(${CARD.style.dynamic.badge.backgroundColor.var}, ${CARD.style.dynamic.badge.backgroundColor.default}); } .${CARD.htmlStructure.elements.badge.container.class} .${CARD.htmlStructure.elements.badge.icon.class} { display: flex; align-items: center; justify-content: center; width: var(--badge-icon-size); height: var(--badge-icon-size); color: var(${CARD.style.dynamic.badge.color.var}, ${CARD.style.dynamic.badge.color.default}); } /* ============================================================================= VISIBILITY CONTROLS ============================================================================= */ .${CARD.style.dynamic.hiddenComponent.icon.class} :is(.${CARD.htmlStructure.sections.icon.class}, .${CARD.htmlStructure.elements.shape.class}), .${CARD.style.dynamic.hiddenComponent.name.class} .${CARD.htmlStructure.elements.nameContent.class}, .${CARD.style.dynamic.hiddenComponent.secondary_info.class} .${CARD.htmlStructure.elements.secondaryInfoWrapper.class}, .${CARD.style.dynamic.hiddenComponent.progress_bar.class} .${CARD.htmlStructure.elements.progressBar.bar.class}, .${CARD.style.dynamic.hiddenComponent.shape.class} .${CARD.htmlStructure.elements.shape.class} ha-ripple { display: none; } /* Shape transparency when hidden */ .${CARD.style.dynamic.hiddenComponent.shape.class} .${CARD.htmlStructure.elements.shape.class} { background-color: transparent; } /* Show elements when needed */ .${CARD.style.dynamic.show}-${CARD.htmlStructure.elements.badge.container.class} .${CARD.htmlStructure.elements.badge.container.class} { display: flex; } /* ============================================================================= INTERACTIVE STATES ============================================================================= */ .${CARD.style.dynamic.clickable.card}:hover, .${CARD.style.dynamic.clickable.icon} .${CARD.htmlStructure.sections.icon.class}:hover { cursor: pointer; } /* ============================================================================= single line ============================================================================= */ .overlay.single-line { --group-max-width: 100%; --group-width: 100%; justify-content: space-between; flex-direction: row; align-items: center; } .overlay.single-line .${CARD.htmlStructure.elements.secondaryInfoWrapper.class} { --group-max-width: none; margin-right: 7px; } /* ============================================================================= TRANSFORMATION VERTICALE - ORIENTATION DU BAS VERS LE HAUT ============================================================================= */ .vertical.up-orientation .container { height: 100%; } /* === prefers-reduced-motion === */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0ms !important; scroll-behavior: auto !important; } } `; /****************************************************************************************** * 📦 Test utils ******************************************************************************************/ const is = { nullish: (val) => val == null, // null or undefined boolean: (val) => typeof val === 'boolean', string: (val) => typeof val === 'string', emptyString: (val) => typeof val === 'string' && val.trim() === '', nonEmptyString: (val) => typeof val === 'string' && val.trim() !== '', nullishOrEmptyString: (val) => val == null || (typeof val === 'string' && val.trim() === ''), numericString: (val) => typeof val === 'string' && val.trim() !== '' && !isNaN(parseFloat(val)), number: (val) => Number.isFinite(val), integer: (val) => typeof val === 'number' && Number.isInteger(val) && val >= 0, func: (val) => typeof val === 'function', object: (val) => typeof val === 'object', plainObject: (val) => typeof val === 'object' && val !== null && !Array.isArray(val), array: (val) => Array.isArray(val), nonEmptyArray: (val) => Array.isArray(val) && val.length > 0, nonEmptySet: (val) => val instanceof Set && val.size > 0, jinja: (val) => { if (typeof val !== 'string') return false; const jinjaPattern = /({{.*?}}|{#.*?#}|{%.+?%})/s; return jinjaPattern.test(val); }, }; const has = { own: (obj, key) => Object.hasOwn(obj, key), method: (obj, key) => typeof obj?.[key] === 'function', validKey: (obj, key) => typeof key === 'string' && key !== '' && has.own(obj, key), }; /****************************************************************************************** * 📦 Logging utils ******************************************************************************************/ /****************************************************************************************** * 🛠️ Logger */ const Logger = { create(name, level = SEV.debug) { const levels = { error: 0, warning: 1, info: 2, debug: 3 }; const currentLevel = levels[level] || 3; const shouldLog = (logLevel) => levels[logLevel] <= currentLevel; const loggerInstance = { name, level, debug: (msg, data) => shouldLog(SEV.debug) && console.debug(`[${name}] ${msg}`, ...(data !== undefined ? [data] : [])), info: (msg, data) => shouldLog(SEV.info) && console.info(`[${name}] ${msg}`, ...(data !== undefined ? [data] : [])), warning: (msg, data) => shouldLog(SEV.warning) && console.warn(`[${name}] ${msg}`, ...(data !== undefined ? [data] : [])), error: (msg, data) => shouldLog(SEV.error) && console.error(`[${name}] ${msg}`, ...(data !== undefined ? [data] : [])), wrap: (fn, fnName) => { const isAsync = fn.constructor.name === 'AsyncFunction'; const logStart = () => shouldLog(SEV.debug) && console.debug(`[${name}] 👉 ${fnName}`); const logSuccess = (start) => shouldLog(SEV.debug) && console.debug(`[${name}] ✅ ${fnName} (${(performance.now() - start).toFixed(2)}ms)`); const logError = (start, error) => shouldLog(SEV.error) && console.error(`[${name}] ❌ ${fnName} failed (${(performance.now() - start).toFixed(2)}ms)`, error); if (isAsync) { return async (...args) => { logStart(); const start = performance.now(); try { const result = await fn(...args); logSuccess(start); return result; } catch (error) { logError(start, error); throw error; } }; } else { return (...args) => { logStart(); const start = performance.now(); try { const result = fn(...args); logSuccess(start); return result; } catch (error) { logError(start, error); throw error; } }; } }, wrapAll: (ctx, methodNames) => { methodNames.forEach((method) => { if (has.method(ctx, method)) { ctx[method] = loggerInstance.wrap(ctx[method].bind(ctx), method); } }); }, state: (label, hass, config) => { if (!shouldLog(SEV.debug)) return; console.debug(`[${name}] 📊 ${label}`, { hasHass: Boolean(hass), hasConfig: Boolean(config), entities: config?.entities?.length || 0, connected: document.body.contains ? 'unknown' : 'checking', }); }, }; return loggerInstance; }, }; function initLogger(ctx, debugFlag, methodNames = []) { const className = ctx.constructor.name; const logger = Logger.create(className, debugFlag ? SEV.debug : SEV.info); if (debugFlag) { logger.wrapAll(ctx, methodNames); logger.debug(`${className} initialized`); } return logger; } /****************************************************************************************** * 📦 Components registration ******************************************************************************************/ /****************************************************************************************** * 🛠️ RegistrationHelper * ======================================================================================== * * ✅ Helper to register component. * * @class */ class RegistrationHelper { static _devMode = CARD_CONTEXT.dev; static #targetKey = { customCards: 'customCards', customBadges: 'customBadges', customCardFeatures: 'customCardFeatures', }; static #resolveComponent(component) { if (!RegistrationHelper._devMode) return component; return { ...component, typeName: `${component.typeName}-dev`, name: `${component.name} (dev)`, editor: component.editor ? `${component.editor}-dev` : undefined, }; } static #resolveEntry(component, targetKey) { return targetKey === RegistrationHelper.#targetKey.customCardFeatures ? { type: component.typeName, name: component.name, supported: () => true } : { type: component.typeName, name: component.name, preview: true, description: component.description, documentationURL: META.documentation, version: VERSION, }; } static #registerComponent(component, targetKey, elementClass, editorClass) { try { // On tente l'enregistrement technique if (!customElements.get(component.typeName)) customElements.define(component.typeName, elementClass); if (editorClass && component.editor && !customElements.get(component.editor)) customElements.define(component.editor, editorClass); } catch (error) { // Si ça échoue (déjà défini), on log mais on ne bloque pas la suite console.warn(`[Entity Progress Card] Registration alert: ${error.message}`); } // Le reste du code est protégé const registerUI = () => { try { window[targetKey] = window[targetKey] || []; if (window[targetKey].some((item) => item.type === component.typeName)) return; window[targetKey].push(RegistrationHelper.#resolveEntry(component, targetKey)); } catch (uiError) { console.error('[Entity Progress Card] UI Registration failed', uiError); } }; setTimeout(registerUI, 1000); } static registerCard(card, elementClass, editorClass) { RegistrationHelper.#registerComponent( RegistrationHelper.#resolveComponent(card), RegistrationHelper.#targetKey.customCards, elementClass, editorClass, ); } static registerBadge(badge, elementClass, editorClass) { RegistrationHelper.#registerComponent( RegistrationHelper.#resolveComponent(badge), RegistrationHelper.#targetKey.customBadges, elementClass, editorClass, ); } static registerCardFeature(cardFeature, elementClass) { RegistrationHelper.#registerComponent( RegistrationHelper.#resolveComponent(cardFeature), RegistrationHelper.#targetKey.customCardFeatures, elementClass, ); } } /****************************************************************************************** * 📦 CARD LIB ******************************************************************************************/ const CONTENT_SLOT = '{{content}}'; const Element = (obj, extraClass = '') => { const className = `${obj.class} ${extraClass}`.trim(); const renderAttrs = (attrsObj = {}) => Object.entries(attrsObj) .map(([key, value]) => `${key}="${value}"`) .join(' '); return { tag: obj.element, class: className, html: (content = '', attrs = {}) => { const allAttrs = { ...(obj.id ? { id: obj.id } : {}), ...(obj.extraAttr || {}), ...attrs }; return `<${obj.element} class="${className}" ${renderAttrs(allAttrs)}>${content}`; }, }; }; const StructureElements = { ripple: () => '', container: (options) => StructureElements.ripple() + Element(CARD.htmlStructure.sections.container, options.layout).html(CONTENT_SLOT), belowContainer: () => Element(CARD.htmlStructure.sections.belowContainer).html(CONTENT_SLOT), topContainer: () => Element(CARD.htmlStructure.sections.topContainer).html(CONTENT_SLOT), backgroundContainer: () => Element(CARD.htmlStructure.sections.backgroundContainer).html(CONTENT_SLOT), bottomContainer: () => Element(CARD.htmlStructure.sections.bottomContainer).html(CONTENT_SLOT), iconAndShape: () => Element(CARD.htmlStructure.elements.shape).html(StructureElements.ripple() + Element(CARD.htmlStructure.elements.icon).html()), badge: () => Element(CARD.htmlStructure.elements.badge.container).html(Element(CARD.htmlStructure.elements.badge.icon).html()), nameContent: (minimal = false) => Element(CARD.htmlStructure.elements.nameContent).html( Element(CARD.htmlStructure.elements.ellipsisWrapper).html( Element(CARD.htmlStructure.elements.nameValue).html( Element(CARD.htmlStructure.elements.nameMain).html() + (minimal ? '' : Element(CARD.htmlStructure.elements.nameExtra).html()), ), ), ), secondaryInfoWrapper: () => Element(CARD.htmlStructure.elements.secondaryInfoWrapper).html( Element(CARD.htmlStructure.elements.ellipsisWrapper).html( Element(CARD.htmlStructure.elements.secondaryInfoValue).html( Element(CARD.htmlStructure.elements.secondaryInfoExtra).html() + Element(CARD.htmlStructure.elements.secondaryInfoMain).html(), ), ), ), secondaryInfoWrapperMinimal: () => Element(CARD.htmlStructure.elements.secondaryInfoWrapper).html( Element(CARD.htmlStructure.elements.ellipsisWrapper).html( Element(CARD.htmlStructure.elements.secondaryInfoValue).html(Element(CARD.htmlStructure.elements.secondaryInfoExtra).html()), ), ), progressBar: (options) => { const extraClass = options.barPosition === 'overlay' ? 'overlay' : ''; const isCenterZero = options.barType === 'centerZero'; const marks = Element(CARD.htmlStructure.elements.progressBar.lowWatermark, 'watermark mark').html() + Element(CARD.htmlStructure.elements.progressBar.highWatermark, 'watermark mark').html() + (isCenterZero ? Element(CARD.htmlStructure.elements.progressBar.zeroMark, 'mark').html() : ''); const innerHtml = Element(CARD.htmlStructure.elements.progressBar.inner).html() + marks; return Element(CARD.htmlStructure.elements.progressBar.container, extraClass).html( Element(CARD.htmlStructure.elements.progressBar.bar, isCenterZero ? CARD.style.dynamic.progressBar.centerZero.class : 'default').html(innerHtml), isCenterZero ? { 'aria-valuemin': '-100' } : {} ); }, createSecondaryInfo: (options, secondaryInfoWrapperFn) => { const { layout, barPosition } = options; const excludedPositions = ['top', 'bottom', 'below', 'overlay', 'background']; const excludedLayouts = ['vertical']; let content = secondaryInfoWrapperFn(); if (!excludedPositions.includes(barPosition) && !excludedLayouts.includes(layout)) { content += StructureElements.progressBar(options); } return Element(CARD.htmlStructure.elements.secondaryInfo).html(content); }, secondaryInfo: (options) => StructureElements.createSecondaryInfo(options, StructureElements.secondaryInfoWrapper), secondaryInfoMinimal: (options) => StructureElements.createSecondaryInfo(options, StructureElements.secondaryInfoWrapperMinimal), createContent: (options, rightContent) => { const isOverlay = options.barPosition === 'overlay'; const isSingleLine = options.barSingleLine; const isVertical = options.layout === 'vertical'; const isBelowTopOrBottom = ['below', 'top', 'bottom', 'background'].includes(options.barPosition); const extraClass = (isOverlay ? ' overlay' : '') + (isSingleLine ? ' single-line' : ''); const before = isOverlay ? StructureElements.progressBar(options) : ''; const after = !isOverlay && !isBelowTopOrBottom && isVertical ? StructureElements.progressBar(options) : ''; const content = before + rightContent + after; return Element(CARD.htmlStructure.sections.content, extraClass).html(content); }, contentFull: (options) => StructureElements.createContent(options, StructureElements.nameContent() + StructureElements.secondaryInfo(options)), contentMini: (options) => StructureElements.createContent(options, StructureElements.nameContent(true) + StructureElements.secondaryInfoMinimal(options)), iconSection: () => Element(CARD.htmlStructure.sections.icon).html(StructureElements.iconAndShape() + StructureElements.badge()), iconSectionWoBadge: () => Element(CARD.htmlStructure.sections.icon).html(StructureElements.iconAndShape()), trendIndicator: (options) => options.trendIndicator ? Element(CARD.htmlStructure.elements.trendIndicator.container).html(Element(CARD.htmlStructure.elements.trendIndicator.icon).html()) : '', wrapWithBarPosition: (content, options) => { const { barPosition } = options; const bar = () => StructureElements.progressBar(options); const wrap = { top: () => ({ before: StructureElements.topContainer().replace(CONTENT_SLOT, bar()), after: '' }), bottom: () => ({ before: '', after: StructureElements.bottomContainer().replace(CONTENT_SLOT, bar()) }), below: () => ({ before: '', after: StructureElements.belowContainer().replace(CONTENT_SLOT, bar()) }), background: () => ({ before: '', after: StructureElements.backgroundContainer().replace(CONTENT_SLOT, bar()) }), }; const { before = '', after = '' } = wrap[barPosition]?.() ?? {}; return before + content + after; }, }; const StructureTemplates = { card: (options = {}) => { return StructureElements.wrapWithBarPosition( StructureElements.container(options).replace( CONTENT_SLOT, StructureElements.trendIndicator(options) + StructureElements.iconSection() + StructureElements.contentFull(options), ), options, ); }, badge: (options = {}) => { return StructureElements.container(options).replace( CONTENT_SLOT, StructureElements.iconSectionWoBadge() + StructureElements.contentFull(options), ); }, template: (options = {}) => { return StructureElements.wrapWithBarPosition( StructureElements.container(options).replace( CONTENT_SLOT, StructureElements.trendIndicator(options) + StructureElements.iconSection() + StructureElements.contentMini(options), ), options, ); }, feature: (options = {}) => { const { barPosition } = options; const bar = () => StructureElements.progressBar(options); const containers = { top: () => StructureElements.topContainer().replace(CONTENT_SLOT, bar()), bottom: () => StructureElements.bottomContainer().replace(CONTENT_SLOT, bar()), }; return containers[barPosition]?.() ?? bar(); }, }; class ObjStructure { _options = {}; _cardType = 'card'; get options() { return this._options; } set options(newOptions) { this._options = newOptions; } get innerHTML() { return StructureTemplates[this._cardType](this.options); } } class CardStructure extends ObjStructure { _cardType = 'card'; } class BadgeStructure extends ObjStructure { _cardType = 'badge'; } class TemplateStructure extends ObjStructure { _cardType = 'template'; } class FeatureStructure extends ObjStructure { _cardType = 'feature'; } /****************************************************************************************** * 🛠️ NumberFormatter * ======================================================================================== * * ✅ class for formatting value && unit. * * This class uses `Value`, `Unit`, and `Decimal` objects to manage and validate its * internal data. * * @class */ class NumberFormatter { static unitsNoSpace = { 'fr-FR': new Set(['j', 'd', 'h', 'min', 'ms', 'μs', '°']), 'de-DE': new Set(['d', 'h', 'min', 'ms', 'μs', '°']), 'en-US': new Set(['d', 'h', 'min', 'ms', 'μs', '°', '%']), }; static getSpaceCharacter(locale, unit) { const set = NumberFormatter.unitsNoSpace[locale] || NumberFormatter.unitsNoSpace['en-US']; return set.has(unit.toLowerCase()) ? '' : CARD.config.unit.space; } static formatValueAndUnit(value, decimal = 2, unit = '', locale = 'en-US', unitSpacing = CARD.config.unit.unitSpacing.auto) { if (is.nullish(value)) return ''; const formattedValue = new Intl.NumberFormat(locale, { minimumFractionDigits: decimal, maximumFractionDigits: decimal, useGrouping: locale !== 'en', }).format(value); if (!unit) return formattedValue; const spaceMap = { space: CARD.config.unit.space, 'no-space': '', auto: () => NumberFormatter.getSpaceCharacter(locale, unit), }; const space = has.method(spaceMap, unitSpacing) ? spaceMap[unitSpacing]() : spaceMap[unitSpacing]; return `${formattedValue}${space}${unit}`; } static formatTiming(totalSeconds, decimal = 0, locale = 'en-US', flex = false, unitSpacing = CARD.config.unit.unitSpacing.auto) { const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); let seconds = (totalSeconds % 60).toFixed(decimal); const pad = (value, length = 2) => String(value).padStart(length, '0'); const [intPart, decimalPart] = seconds.split('.'); seconds = decimalPart !== undefined ? `${pad(intPart)}.${decimalPart}` : pad(seconds); if (flex) { if (totalSeconds < 60) return NumberFormatter.formatValueAndUnit(parseFloat(seconds), decimal, 's', locale, unitSpacing); if (totalSeconds < 3600) return `${pad(minutes)}:${seconds}`; } return [pad(hours), pad(minutes), seconds].join(':'); } static durationToSeconds(value, unit) { switch (unit) { case 'd': // Jour return value * 86400; // 1 jour = 86400 secondes case 'h': // Heure return value * 3600; // 1 heure = 3600 secondes case 'min': // Minute return value * 60; // 1 minute = 60 secondes case 's': // Seconde return value; // 1 seconde = 1 seconde case 'ms': // Milliseconde return value * 0.001; // 1 milliseconde = 0.001 seconde case 'μs': // Microseconde return value * 0.000001; // 1 microseconde = 0.000001 seconde default: throw new Error('Unknown case'); } } static convertDuration(duration) { const parts = duration.split(':').map(Number); const [hours, minutes, seconds] = parts; return (hours * 3600 + minutes * 60 + seconds) * CARD.config.msFactor; } } /****************************************************************************************** * 🛠️ ValueHelper * ======================================================================================== * * ✅ Helper class for managing numeric values. * This class validates and stor a numeric value. * * @class */ class ValueHelper { #value = null; #isValid = false; #defaultValue = null; // ─── LIFECYCLE ──────────────────────────────────────────────────────────── constructor(newValue = null) { if (ValueHelper.#validate(newValue)) this.#defaultValue = newValue; } // ─── PUBLIC GETTERS / SETTERS ───────────────────────────────────────────── set value(newValue) { this.#isValid = ValueHelper.#validate(newValue); // Appel à la méthode statique this.#value = this.#isValid ? newValue : null; } get value() { return this.#isValid ? this.#value : this.#defaultValue; } get isValid() { return this.#isValid; } // ─── VALIDATION ─────────────────────────────────────────────────────────── static #validate(value) { return Number.isFinite(value); } } /****************************************************************************************** * 🛠️ DecimalHelper * ======================================================================================== * * ✅ Represents a non-negative integer value that can be valid or invalid. * * @class */ class DecimalHelper { #value = CARD.config.decimal.percentage; #isValid = false; #defaultValue = null; constructor(newValue = null) { if (DecimalHelper.#validate(newValue)) this.#defaultValue = newValue; } // ─── PUBLIC GETTERS / SETTERS ───────────────────────────────────────────── set value(newValue) { this.#isValid = DecimalHelper.#validate(newValue); this.#value = this.#isValid ? newValue : null; } get value() { return this.#isValid ? this.#value : this.#defaultValue; } get isValid() { return this.#isValid; } // ─── VALIDATION ─────────────────────────────────────────────────────────── static #validate(value) { return Number.isInteger(value) && value >= 0; } } /****************************************************************************************** * 🛠️ UnitHelper * ======================================================================================== * * ✅ Represents a unit of measurement, stored as a string. * * @class */ class UnitHelper { #value = CARD.config.unit.default; #isDisabled = false; // ─── PUBLIC GETTERS / SETTERS ───────────────────────────────────────────── set value(newValue) { this.#value = newValue.trim() ?? CARD.config.unit.default; } get value() { return this.#isDisabled ? '' : this.#value; } set isDisabled(newValue) { this.#isDisabled = is.boolean(newValue) ? newValue : false; } get isDisabled() { return this.#isDisabled; } get isTimerUnit() { return this.#value.toLowerCase() === CARD.config.unit.timer; } get isFlexTimerUnit() { return this.#value.toLowerCase() === CARD.config.unit.flexTimer; } // ─── PUBLIC API METHODS ─────────────────────────────────────────────────── toString() { return this.#isDisabled ? '' : this.#value; } } /****************************************************************************************** * 🛠️ PercentHelper * ======================================================================================== * * ✅ class for calculating and formatting percentages. * * @class */ class PercentHelper { #hassProvider = null; #min = new ValueHelper(CARD.config.value.min); #max = new ValueHelper(CARD.config.value.max); #current = new ValueHelper(0); #unit = new UnitHelper(); #decimal = new DecimalHelper(CARD.config.decimal.percentage); #percent = 0; #isTimer = false; #isReversed = false; #isCenterZero = false; unitSpacing = CARD.config.unit.unitSpacing.auto; constructor() { this.#hassProvider = HassProviderSingleton.getInstance(); } // ─── PUBLIC GETTERS / SETTERS ───────────────────────────────────────────── set isTimer(newValue) { this.#isTimer = is.boolean(newValue) ? newValue : false; } get isTimer() { return this.#isTimer; } set isReversed(newValue) { this.#isReversed = is.boolean(newValue) ? newValue : CARD.config.reverse; } get isReversed() { return this.#isReversed; } set min(newValue) { this.#min.value = newValue; } get min() { return this.#min.value; } set max(newValue) { this.#max.value = newValue; } get max() { return this.#max.value; } set current(newCurrent) { this.#current.value = newCurrent; } get current() { return this.#current.value; } get actual() { return this.#isReversed ? this.max - this.current : this.current; } get unit() { return this.#unit.value; } set unit(newValue) { this.#unit.value = newValue ?? ''; } set hasDisabledUnit(newValue) { this.#unit.isDisabled = newValue; } get hasDisabledUnit() { return this.#unit.isDisabled; } set decimal(newValue) { this.#decimal.value = newValue; } get decimal() { return this.#decimal.value; } get isValid() { return this.range !== 0; } get range() { if (!this.isCenterZero) return this.max - this.min; return this.current >= 0 ? this.max : -this.min; } get correctedValue() { return this.isCenterZero ? this.current : this.actual - this.min; } get percent() { return this.isValid ? this.#percent : null; } get hasTimerUnit() { return this.#isTimer && this.#unit.isTimerUnit; } get hasFlexTimerUnit() { return this.#isTimer && this.#unit.isFlexTimerUnit; } get hasTimerOrFlexTimerUnit() { return this.hasTimerUnit || this.hasFlexTimerUnit; } get processedValue() { return this.unit === CARD.config.unit.default ? this.percent : this.actual; } set isCenterZero(newValue) { this.#isCenterZero = is.boolean(newValue) ? newValue : false; } get isCenterZero() { return this.#isCenterZero; } // ─── PUBLIC API METHODS ─────────────────────────────────────────────────── valueForThemes(isCustomTheme, valueBasedOnPercentage) { /* * Calculates the value to display based on the selected theme and unit system. * * - If the unit is Fahrenheit, the temperature is converted to Celsius before returning. * - If the theme is linear or the unit is the default, the percentage value is returned. */ let value = this.actual; if (isCustomTheme) return value; if (this.unit === CARD.config.unit.fahrenheit) value = ((value - 32) * 5) / 9; return valueBasedOnPercentage || [CARD.config.unit.default, CARD.config.unit.disable].includes(this.unit) ? this.percent : value; } refresh() { this.#percent = this.isValid ? Number(((this.correctedValue / this.range) * 100).toFixed(this.decimal)) : 0; } calcWatermark(value) { return [CARD.config.unit.default, CARD.config.unit.disable].includes(this.unit) ? value : ((value - this.min) / this.range) * 100; } toString() { if (!this.isValid) { return 'Div0'; } else if (this.hasTimerOrFlexTimerUnit) { // timer with time format return NumberFormatter.formatTiming(this.actual, this.decimal, this.#hassProvider.numberFormat, this.hasFlexTimerUnit, this.unitSpacing); } return NumberFormatter.formatValueAndUnit(this.processedValue, this.decimal, this.unit, this.#hassProvider.numberFormat, this.unitSpacing); } } /****************************************************************************************** * 🛠️ ThemeManager * ======================================================================================== * * ✅ Manages the theme and its associated icon and color based on a percentage value. * * @class */ class ThemeManager { #theme = null; #icon = null; #iconColor = null; #barColor = null; #value = 0; #isValid = false; #isLinear = false; #isBasedOnPercentage = false; #isCustomTheme = false; #currentStyle = null; #interpolate = false; // ─── PUBLIC GETTERS / SETTERS ───────────────────────────────────────────── set theme(newTheme) { if (is.nullish(newTheme) || !has.validKey(THEME, newTheme)) { this.#reset(); return; } this.#isValid = true; this.#theme = newTheme; this.#currentStyle = THEME[newTheme].style; this.#isLinear = THEME[newTheme].linear; this.#isBasedOnPercentage = THEME[newTheme].percent; } get theme() { return this.#theme; } set customTheme(newTheme) { if (!ThemeManager.#validateCustomTheme(newTheme)) { return; } this.#theme = CARD.theme.default; this.#currentStyle = newTheme; this.#isValid = true; this.#isLinear = false; this.#isCustomTheme = true; } get customTheme() { return this.#currentStyle; } set interpolate(newInterpolate) { this.#interpolate = newInterpolate; } get interpolate() { return this.#interpolate; } get isLinear() { return this.#isLinear; } get isBasedOnPercentage() { return this.#isBasedOnPercentage; } get isCustomTheme() { return this.#isCustomTheme; } get isValid() { return this.#isValid; } set value(newValue) { this.#value = newValue; this.#refresh(); } get value() { return this.#value; } get icon() { return this.#icon; } get iconColor() { return this.#iconColor; } get barColor() { return this.#barColor; } // ─── PRIVATE METHODS ────────────────────────────────────────────────────── #reset() { [ this.#icon, this.#barColor, this.#iconColor, this.#theme, this.#currentStyle, this.#value, this.#isValid, this.#isLinear, this.#isBasedOnPercentage, this.#isCustomTheme, this.#interpolate, ] = [null, null, null, null, null, 0, false, false, false, false, false]; } #refresh() { if (!this.#isValid) return; const applyStyle = this.isLinear ? this.#setLinearStyle : this.#setStyle; applyStyle.call(this); } #setLinearStyle() { const lastStep = this.#currentStyle.length - 1; const thresholdSize = CARD.config.value.max / lastStep; const percentage = Math.max(0, Math.min(this.#value, CARD.config.value.max)); const index = Math.min(Math.floor(percentage / thresholdSize), lastStep); const ratio = (percentage - index * thresholdSize) / thresholdSize; this.#applyColors(this.#currentStyle[index], this.#currentStyle[index + 1] ?? null, ratio); } #setStyle() { // eslint-disable-next-line no-useless-assignment let [themeData, nextThemeData, ratio] = [null, null, 0]; if (this.#value >= this.#currentStyle[this.#currentStyle.length - 1].max) { themeData = this.#currentStyle[this.#currentStyle.length - 1]; } else if (this.#value < this.#currentStyle[0].min) { themeData = this.#currentStyle[0]; } else { const index = this.#currentStyle.findIndex((level) => this.#value >= level.min && this.#value < level.max); themeData = this.#currentStyle[index]; nextThemeData = this.#currentStyle[index + 1] ?? null; ratio = (this.#value - themeData.min) / (themeData.max - themeData.min); } this.#applyColors(themeData, nextThemeData, ratio); } #applyColors(themeData, nextThemeData, ratio) { this.#icon = themeData.icon || null; if (this.#interpolate && nextThemeData) { const color = ThemeManager.#interpolateColor( ThemeManager.adaptColor(themeData.icon_color || themeData.color || null), ThemeManager.adaptColor(nextThemeData.icon_color || nextThemeData.color || null), ratio, ); const barColor = ThemeManager.#interpolateColor( ThemeManager.adaptColor(themeData.bar_color || themeData.color || null), ThemeManager.adaptColor(nextThemeData.bar_color || nextThemeData.color || null), ratio, ); this.#iconColor = color; this.#barColor = barColor; } else { this.#iconColor = ThemeManager.adaptColor(themeData.icon_color || themeData.color || null); this.#barColor = ThemeManager.adaptColor(themeData.bar_color || themeData.color || null); } } static #interpolateColor(from, to, ratio) { if (!from || !to) return null; const pct = Math.round(ratio * 100); return `color-mix(in srgb, ${to} ${pct}%, ${from})`; // from/to déjà adaptés } static #validateCustomTheme(customTheme) { if (!is.nonEmptyArray(customTheme)) return false; let isFirstItem = true; let lastMax = null; return customTheme.every((item) => { if (item === null || typeof item !== 'object') return false; if (!CARD.theme.customTheme.expectedKeys.every((key) => key in item)) return false; if (!CARD.theme.customTheme.colorKeys.some((key) => key in item)) return false; if (item.min >= item.max) return false; if (!isFirstItem && item.min !== lastMax) return false; isFirstItem = false; lastMax = item.max; return true; }); } // ─── PUBLIC API METHODS ─────────────────────────────────────────────────── static adaptColor(curColor) { return HA_CONTEXT.haColors.get(curColor) ?? curColor; } } /****************************************************************************************** * 🛠️ HassProviderSingleton * ======================================================================================== * * ✅ Provides access to the Home Assistant object. * This class implements a singleton pattern to ensure only one instance exists. * * @class */ class HassProviderSingleton { static #instance = null; static #allowInit = false; static #entityMap = { device_class: { source: 'attribute' }, friendly_name: { source: 'attribute' }, icon: { source: 'attribute' }, unit_of_measurement: { source: 'attribute' }, finishes_at: { source: 'attribute' }, duration: { source: 'attribute' }, remaining: { source: 'attribute' }, entity_picture: { source: 'attribute' }, state: { source: 'state' }, last_changed: { source: 'state' }, last_updated: { source: 'state' }, display_precision: { source: 'entity' }, }; #debug = CARD_CONTEXT.debug.hass; #log = null; #hass = null; #isValid = false; #translations = {}; #rtf = null; #rtfLanguage = null; constructor() { if (!HassProviderSingleton.#allowInit) { throw new Error('Use HassProviderSingleton.getInstance() instead of new.'); } this.#log = Logger.create('HassProviderSingleton', this.#debug ? SEV.debug : SEV.info); HassProviderSingleton.#allowInit = false; } // ─── PUBLIC GETTERS / SETTERS ───────────────────────────────────────────── set hass(hass) { if (!hass) return; const firstHass = this.#hass === null; const previousLanguage = this.language; this.#hass = hass; const currentLanguage = this.language; if (firstHass || previousLanguage !== currentLanguage) this.#loadTranslations(currentLanguage); this.#isValid = true; this.#log.debug('HASS updated!'); } get hass() { return this.#hass; } get isValid() { return this.#isValid; } get language() { return this.#hass?.language in TRANSLATIONS ? this.#hass.language : CARD.config.language; } getMessage(code) { return this.localize('card.msg')[code] || `Unknown message code: ${code}`; } get numberFormat() { const localeFromLang = (lang) => { try { return new Intl.NumberFormat(lang).resolvedOptions().locale; } catch { return 'en-US'; } }; const userDef = this.#hass?.locale?.number_format; const numberFormatMap = { ...HA_CONTEXT.numberFormat, language: localeFromLang(this.language), system: Intl.NumberFormat().resolvedOptions().locale, none: 'en', }; return numberFormatMap[userDef] || localeFromLang(this.language); } get version() { return this.#hass?.config?.version ?? null; } get hasNewShapeStrategy() { const [year, month] = (this.version ?? '0.0').split('.').map(Number); return year > 2025 || (year === 2025 && month >= 3); } // ─── PUBLIC API METHODS ─────────────────────────────────────────────────── localize(key) { const result = key.split('.').reduce((obj, k) => obj?.[k], this.#translations); return result ?? key; } static getInstance() { if (!HassProviderSingleton.#instance) { HassProviderSingleton.#allowInit = true; HassProviderSingleton.#instance = new HassProviderSingleton(); } return HassProviderSingleton.#instance; } getEntityProp(entityId, prop, format = false) { return format ? this.#formatEntityProp(entityId, prop) : this.#resolveEntityProp(entityId, prop); } #resolveEntityProp(entityId, prop) { const mapping = HassProviderSingleton.#entityMap[prop]; if (!mapping) return null; const resolvers = { attribute: () => this.getEntityAttribute(entityId, prop), state: () => this.getEntityStateObj(entityId)?.[prop] ?? null, entity: () => this.#hass?.entities?.[entityId]?.[prop] ?? null, }; return resolvers[mapping.source]?.() ?? null; } #formatEntityProp(entityId, prop) { if (prop === 'last_changed' || prop === 'last_updated') return this.getRelativeTime(this.#resolveEntityProp(entityId, prop)); const stateObj = this.getEntityStateObj(entityId); if (prop === 'state') return stateObj ? (this.#hass?.formatEntityState?.(stateObj) ?? '') : this.localize('card.msg.entityNotFound'); return this.#hass?.formatEntityAttributeValue?.(stateObj, prop) ?? ''; } hasEntity(entityId) { return entityId in (this.#hass?.states || {}); } getEntityStateObj(entityId) { return this.#hass?.states?.[entityId] ?? null; } #getAttributes(entityId) { return this.getEntityStateObj(entityId)?.attributes ?? {}; } getEntityAttribute(entityId, attribute) { if (!attribute) return null; const attributes = this.#getAttributes(entityId); return attribute in attributes ? attributes[attribute] : null; } getEntityName(entityId) { return this.#hass?.entities?.[entityId].name; } getEntityDevice(entityId) { const deviceId = this.#hass?.entities?.[entityId]?.device_id; if (!deviceId) return null; return this.#hass?.devices?.[deviceId]?.name ?? null; } getEntityArea(entityId) { const entityAreaId = this.#hass?.entities?.[entityId]?.area_id; if (entityAreaId) return this.#hass?.areas?.[entityAreaId]?.name ?? null; const deviceId = this.#hass?.entities?.[entityId]?.device_id; if (!deviceId) return null; const deviceAreaId = this.#hass?.devices?.[deviceId]?.area_id; return this.#hass?.areas?.[deviceAreaId]?.name ?? null; } getEntityFloor(entityId) { const areaId = this.#hass?.entities?.[entityId]?.area_id ?? this.#hass?.devices?.[this.#hass?.entities?.[entityId]?.device_id]?.area_id; if (!areaId) return null; const floorId = this.#hass?.areas?.[areaId]?.floor_id; return this.#hass?.floors?.[floorId]?.name ?? null; } static getEntityDomain(entityId) { return is.string(entityId) && entityId.includes('.') ? entityId.split('.')[0] : null; } isEntityAvailable(entityId) { const state = this.getEntityStateObj(entityId)?.state; return state !== 'unavailable' && state !== 'unknown'; } getRelativeTime(curTime) { if (!curTime) return ''; const startTime = new Date(curTime).getTime(); const now = Date.now(); const diffInSeconds = Math.floor((startTime - now) / 1000); const units = [ { unit: 'year', seconds: 31536000 }, { unit: 'month', seconds: 2592000 }, { unit: 'day', seconds: 86400 }, { unit: 'hour', seconds: 3600 }, { unit: 'minute', seconds: 60 }, { unit: 'second', seconds: 1 }, ]; for (const { unit, seconds } of units) { if (Math.abs(diffInSeconds) >= seconds || unit === 'second') { const value = Math.round(diffInSeconds / seconds); const rtf = this.#getRelativeTimeFormat(); return rtf.format(value, unit); } } return null; } getNumericAttributes(entityId) { return Object.fromEntries( Object.entries(this.#getAttributes(entityId)) .filter(([, val]) => is.number(val) || is.numericString(val)) .map(([key, val]) => [key, is.number(val) ? val : parseFloat(val)]), ); } #loadTranslations(lang) { const curLanguage = has.own(TRANSLATIONS, lang) ? lang : CARD.config.language; this.#translations = TRANSLATIONS[curLanguage]; } #getRelativeTimeFormat() { if (!this.#rtf || this.#rtfLanguage !== this.language) { this.#rtfLanguage = this.language; this.#rtf = new Intl.RelativeTimeFormat(this.language, { numeric: 'auto' }); } return this.#rtf; } } class ChangeTracker { #debug = CARD_CONTEXT.debug.hass; #log = null; #firstTime = true; #watchedEntities = new Set(); #entityCache = {}; #updated = false; #hassState = { isUpdated: false }; constructor() { this.#log = Logger.create('ChangeTracker', this.#debug ? SEV.debug : SEV.info); } // ─── PUBLIC GETTERS / SETTERS ───────────────────────────────────────────── set hassState(hass) { this.#updated = false; if (!hass) return; if (this._hasChanged(hass)) { this._updateCache(hass); this.#updated = true; this.#log.debug('HASS need update...!'); } this.#hassState = { isUpdated: this.#updated }; } get hassState() { return this.#hassState; } get isUpdated() { return this.#updated; } // ─── PRIVATE METHODS ────────────────────────────────────────────────────── _hasChanged(newHass) { if (this.#firstTime) { this.#firstTime = false; return true; } if (!is.nonEmptySet(this.#watchedEntities)) return true; for (const entityId of this.#watchedEntities) { const newState = newHass?.states?.[entityId]; const oldState = this.#entityCache?.[entityId]; if (!newState) return true; if (!oldState || JSON.stringify(newState) !== JSON.stringify(oldState)) { return true; } } return false; } _updateCache(hass) { this.#entityCache = {}; for (const entityId of this.#watchedEntities) { this.#entityCache[entityId] = hass.states?.[entityId] ?? null; } } // ─── PUBLIC API METHODS ─────────────────────────────────────────────────── watchEntity(entityId) { if (entityId) { this.#watchedEntities.add(entityId); } } } /****************************************************************************************** * 🛠️ EntityHelper * ======================================================================================== * * ✅ Helper class for managing entities. * This class validates and retrieves information from Home Assistant if it's an entity. * * @class */ class EntityHelper { #hassProvider = null; #isValid = false; #value = {}; #entityId = null; #attribute = null; #state = null; #domain = null; #entityType = null; #entityTypeFlags = { isTimer: false, isDuration: false, isNumber: false, isCounter: false, isSynced: false }; stateContent = []; #nameTokens = null; static #handleRefreshType = new Map([ [HA_CONTEXT.entity.type.timer, (self) => self._manageTimerEntity()], [HA_CONTEXT.entity.type.duration, (self) => self._manageDurationEntity()], [HA_CONTEXT.entity.type.counter, (self) => self._manageCounterAndNumberEntity('minimum', 'maximum')], [HA_CONTEXT.entity.type.number, (self) => self._manageCounterAndNumberEntity('min', 'max')], [HA_CONTEXT.entity.type.default, (self) => self._manageStdEntity()], ]); constructor() { this.#hassProvider = HassProviderSingleton.getInstance(); } // ─── PUBLIC GETTERS / SETTERS ───────────────────────────────────────────── set entityId(newValue) { this.#entityId = newValue; this.#nameTokens = null; this.#entityType = null; this.#entityTypeFlags.isSynced = false; this.#value = 0; this.#domain = HassProviderSingleton.getEntityDomain(newValue); this.#isValid = this.#hassProvider.hasEntity(this.#entityId); } get entityId() { return this.#entityId; } set attribute(newValue) { this.#attribute = newValue; } get attribute() { return this.#attribute; } set nameTokens(tok) { this.#nameTokens = is.nonEmptyArray(tok) ? tok : null; } get nameTokens() { return this.#nameTokens; } get value() { return this.#isValid ? this.#value : 0; } get state() { return this.#state; } get isValid() { return this.#isValid; } get isAvailable() { return this.#hassProvider.isEntityAvailable(this.#entityId); } get attributes() { return this.#isValid && !this.entityType.isCounter && !this.entityType.isNumber && !this.entityType.isDuration && !this.entityType.isTimer ? this.#hassProvider.getNumericAttributes(this.#entityId) : {}; } get hasAttribute() { return this.#isValid && Object.keys(this.attributes ?? {}).length > 0; } get defaultAttribute() { return HA_CONTEXT.attributeMapping[this.#domain]?.attribute ?? null; } get name() { return this.#hassProvider.getEntityProp(this.#entityId, 'friendly_name'); } _nameResolver() { const resolvers = { text: (item) => item.text, entity: () => this.#hassProvider.getEntityName(this.entityId), device: () => this.#hassProvider.getEntityDevice(this.entityId), area: () => this.#hassProvider.getEntityArea(this.entityId), floor: () => this.#hassProvider.getEntityFloor(this.entityId), }; return this.#nameTokens .map((item) => { const resolver = resolvers[item.type]; if (!resolver) return null; try { return resolver(item); } catch { return null; } }) .filter((v) => is.nonEmptyString(v)) .join(' '); } get nameComposition() { return this.#nameTokens ? this._nameResolver() : this.name; } get stateObj() { return this.#hassProvider.getEntityStateObj(this.#entityId); } get formatedEntityState() { return this.#hassProvider.getEntityProp(this.#entityId, 'state', true); } get unit() { if (!this.#isValid) return null; if (this.entityType.isTimer) return CARD.config.unit.flexTimer; if (this.entityType.isDuration) return CARD.config.unit.second; if (this.entityType.isCounter) return CARD.config.unit.disable; return this.#hassProvider.getEntityProp(this.#entityId, 'unit_of_measurement'); } get precision() { return this.#isValid ? (this.#hassProvider.getEntityProp(this.#entityId, 'display_precision') ?? null) : null; } get entityType() { if (!this.#entityTypeFlags.isSynced) { const type = this.getEntityType(); const key = `is${type.charAt(0).toUpperCase() + type.slice(1)}`; this.#entityTypeFlags = { isTimer: false, isDuration: false, isNumber: false, isCounter: false, isSynced: true }; this.#entityTypeFlags[key] = true; } return this.#entityTypeFlags; } get hasShapeByDefault() { return [HA_CONTEXT.entity.type.light, HA_CONTEXT.entity.type.fan].includes(this.#domain); } get defaultColor() { const colorMap = { [HA_CONTEXT.entity.type.timer]: this.value?.state === HA_CONTEXT.entity.state.active ? CARD.style.color.active : CARD.style.color.inactive, [HA_CONTEXT.entity.type.cover]: this.value > 0 ? CARD.style.color.coverActive : CARD.style.color.inactive, [HA_CONTEXT.entity.type.light]: this.value > 0 ? CARD.style.color.lightActive : CARD.style.color.inactive, [HA_CONTEXT.entity.type.fan]: this.value > 0 ? CARD.style.color.fanActive : CARD.style.color.inactive, [HA_CONTEXT.entity.type.climate]: this.#getClimateColor(), [HA_CONTEXT.entity.class.battery]: this.#getBatteryColor(), }; return colorMap[this.#domain] ?? colorMap[this.#hassProvider.getEntityProp(this.#entityId, 'device_class')] ?? null; } get stateContentToString() { const results = []; for (const attr of this.stateContent) { switch (attr) { case 'state': results.push(this.#hassProvider.getEntityProp(this.#entityId, 'state', true)); break; case 'device_name': results.push(this.#hassProvider.getEntityDevice(this.entityId)); break; case 'area_name': results.push(this.#hassProvider.getEntityArea(this.#entityId)); break; default: results.push(this.#hassProvider.getEntityProp(this.#entityId, attr, true)); break; } } return results.length !== 0 ? results.join(CARD.config.separator) : ''; } // ─── PUBLIC API METHODS ─────────────────────────────────────────────────── getEntityType() { this.#entityType ??= EntityHelper.#handleRefreshType.has(this.#domain) ? this.#domain : this.#hassProvider.getEntityProp(this.#entityId, 'device_class') === HA_CONTEXT.entity.type.duration && !this.#attribute ? HA_CONTEXT.entity.type.duration : HA_CONTEXT.entity.type.default; return this.#entityType; } refresh() { this.#isValid = this.#hassProvider.hasEntity(this.#entityId); if (!this.#isValid) { this.#state = HA_CONTEXT.entity.state.notFound; return; } this.#isValid = this.#attribute ? this.#isValid && this.#hassProvider.getEntityAttribute(this.#entityId, this.#attribute) !== undefined : this.#isValid; this.#state = this.#hassProvider.getEntityProp(this.#entityId, 'state'); if (!this.isValid || !this.isAvailable) return; const type = this.getEntityType(); const handler = EntityHelper.#handleRefreshType.get(type) ?? EntityHelper.#handleRefreshType.get(HA_CONTEXT.entity.type.default); handler(this); } // ─── PRIVATE METHODS ────────────────────────────────────────────────────── _manageStdEntity() { this.#attribute = this.#attribute || HA_CONTEXT.attributeMapping[this.#domain]?.attribute; if (!this.#attribute) { this.#value = parseFloat(this.#state) || 0; return; } const attrValue = this.#hassProvider.getEntityAttribute(this.#entityId, this.#attribute); if (is.numericString(attrValue) || is.number(attrValue)) { this.#value = parseFloat(attrValue); if (this.#domain === HA_CONTEXT.attributeMapping.light.label && this.#attribute === HA_CONTEXT.attributeMapping.light.attribute) { this.#value = (100 * this.#value) / 255; } } else { // Si l'attribut n'est pas trouvé, définir un comportement this.#value = 0; this.#isValid = false; } } _manageTimerEntity() { // eslint-disable-next-line no-useless-assignment let [duration, elapsed] = [null, null]; switch (this.#state) { case HA_CONTEXT.entity.state.idle: { elapsed = CARD.config.value.min; duration = CARD.config.value.max; break; } case HA_CONTEXT.entity.state.active: { const finished_at = new Date(this.#hassProvider.getEntityProp(this.#entityId, 'finishes_at')).getTime(); duration = NumberFormatter.convertDuration(this.#hassProvider.getEntityProp(this.#entityId, 'duration')); const started_at = finished_at - duration; const now = new Date().getTime(); elapsed = now - started_at; break; } case HA_CONTEXT.entity.state.paused: { const remaining = NumberFormatter.convertDuration(this.#hassProvider.getEntityProp(this.#entityId, 'remaining')); duration = NumberFormatter.convertDuration(this.#hassProvider.getEntityProp(this.#entityId, 'duration')); elapsed = duration - remaining; break; } default: throw new Error('Timer entity - Unknown case'); } this.#value = { current: elapsed / CARD.config.msFactor, min: CARD.config.value.min, max: duration / CARD.config.msFactor, state: this.#state }; } _manageCounterAndNumberEntity(min, max) { this.#value = { current: parseFloat(this.state), min: this.#hassProvider.getEntityAttribute(this.#entityId, min), max: this.#hassProvider.getEntityAttribute(this.#entityId, max), }; } _manageDurationEntity() { const unit = this.#hassProvider.getEntityProp(this.#entityId, 'unit_of_measurement'); const value = parseFloat(this.#state); this.#value = unit === undefined ? 0 : NumberFormatter.durationToSeconds(value, unit); this.#isValid = unit !== undefined; } #getClimateColor() { const climateColorMap = { heat_cool: CARD.style.color.active, dry: CARD.style.color.climate.dry, cool: CARD.style.color.climate.cool, heat: CARD.style.color.climate.heat, fan_only: CARD.style.color.climate.fanOnly, }; return climateColorMap[this.#state] || CARD.style.color.inactive; } #getBatteryColor() { if (!this.#value || this.#value <= 30) return CARD.style.color.battery.low; if (this.#value <= 70) return CARD.style.color.battery.medium; return CARD.style.color.battery.high; } } /****************************************************************************************** * 🛠️ EntityCollectionHelper * ======================================================================================== * * ✅ Helper class for managing entities collection. * * @class */ class EntityCollectionHelper { #entities = []; addEntity(entityId, attribute = null) { const helper = new EntityHelper(); helper.entityId = entityId; if (attribute) helper.attribute = attribute; this.#entities.push(helper); } refreshAll() { this.#entities.forEach((helper) => helper.refresh()); } getTotalValue() { return this.#entities .filter((helper) => helper.isValid && helper.isAvailable) .reduce((sum, helper) => { const value = helper.value; return sum + (is.number(value) ? value : (value?.current ?? 0)); }, 0); } getAvailableEntities() { return this.#entities.filter((helper) => helper.isValid && helper.isAvailable); } getPercentages() { const total = this.getTotalValue(); if (total === 0) return []; return this.getAvailableEntities().map((helper) => { const rawValue = helper.value; const value = is.number(rawValue) ? rawValue : (rawValue?.current ?? 0); const percent = (value / total) * 100; return { entityId: helper.entityId, value, percent, }; }); } getEntitiesColor(curColor) { const percentages = this.getPercentages(); if (!percentages.length || !curColor) return null; const total = percentages.length; const gradientStops = []; let currentPosition = 0; for (let i = 0; i < total; i++) { const item = percentages[i]; const whitePercent = Math.round((1 - i / (total - 1 || 1)) * 50); // de 50 → 0 const basePercent = 100 - whitePercent; const color = `color-mix(in srgb, ${curColor} ${basePercent}%, black ${whitePercent}%)`; const start = currentPosition; const end = currentPosition + item.percent; gradientStops.push(`${color} ${start.toFixed(2)}%`, `${color} ${end.toFixed(2)}%`); currentPosition = end; } return `linear-gradient(to right, ${gradientStops.join(', ')})`; } getAvailableCount() { return this.getAvailableEntities().length; } get count() { return this.#entities.length; } get validEntities() { return this.#entities.filter((e) => e.isValid && e.isAvailable); } get all() { return this.#entities; } clear() { this.#entities = []; } } /****************************************************************************************** * 🛠️ EntityOrValue * ======================================================================================== * * ✅ Represents either an entity ID or a direct value. * This class validates the provided value and retrieves information from Home Assistant if it's an entity. * * @class */ class EntityOrValue { #activeHelper = null; // Dynamically set to EntityHelper or ValueHelper #helperType = { entity: 'entity', value: 'value' }; #isEntity = null; #entityTypeFlags = { isTimer: false, isDuration: false, isNumber: false, isCounter: false, isSynced: false }; // ─── PRIVATE METHODS ────────────────────────────────────────────────────── #createHelper(helperType) { const HelperClass = helperType === this.#helperType.entity ? EntityHelper : ValueHelper; if (!(this.#activeHelper instanceof HelperClass)) { this.#activeHelper = new HelperClass(); this.#isEntity = helperType === this.#helperType.entity; } } // ─── PUBLIC GETTERS / SETTERS ───────────────────────────────────────────── set value(newValue) { if (is.string(newValue)) { this.#createHelper(this.#helperType.entity); this.#activeHelper.entityId = newValue; } else if (is.number(newValue)) { this.#createHelper(this.#helperType.value); this.#activeHelper.value = newValue; } else { this.#activeHelper = null; } this.#entityTypeFlags.isSynced = false; } get value() { return this.#activeHelper ? this.#activeHelper.value : null; } get isEntity() { return this.#isEntity; } set attribute(newValue) { if (this.#activeHelper && this.#isEntity) this.#activeHelper.attribute = newValue; } get attribute() { return this.#isEntity ? this.#activeHelper.attribute : null; } set nameTokens(tok) { if (this.#activeHelper && this.#isEntity) this.#activeHelper.nameTokens = tok; } get nameTokens() { return this.#activeHelper && this.#isEntity ? this.#activeHelper.nameTokens : null; } get state() { return this.#activeHelper && this.#isEntity ? this.#activeHelper.state : null; } get isValid() { return this.#activeHelper ? this.#activeHelper.isValid : false; } get isAvailable() { return this.#activeHelper ? (this.#isEntity && this.#activeHelper.isAvailable) || this.#activeHelper.isValid : false; } get precision() { return this.#activeHelper && this.#isEntity ? this.#activeHelper.precision : null; } get name() { return this.#activeHelper && this.#isEntity ? this.#activeHelper.name : null; } get nameComposition() { return this.#activeHelper && this.#isEntity ? this.#activeHelper.nameComposition : null; } get formatedEntityState() { return this.#activeHelper && this.#isEntity ? this.#activeHelper.formatedEntityState : null; } set stateContent(newValue) { if (this.#activeHelper && this.#isEntity) this.#activeHelper.stateContent = newValue; } get stateContent() { return this.#activeHelper && this.#isEntity ? this.#activeHelper.stateContent : null; } get stateContentToString() { return this.#activeHelper && this.#isEntity ? this.#activeHelper.stateContentToString : null; } get entityType() { if (this.#activeHelper && this.#isEntity && !this.#entityTypeFlags.isSynced) this.#entityTypeFlags = this.#activeHelper.entityType; return this.#entityTypeFlags; } get hasShapeByDefault() { return this.#activeHelper && this.#isEntity ? this.#activeHelper.hasShapeByDefault : false; } get defaultColor() { return this.#activeHelper && this.#isEntity ? this.#activeHelper.defaultColor : false; } get hasAttribute() { return this.#activeHelper && this.#isEntity ? this.#activeHelper.hasAttribute : false; } get defaultAttribute() { return this.#activeHelper && this.#isEntity ? this.#activeHelper.defaultAttribute : null; } get attributes() { return this.#activeHelper && this.#isEntity ? this.#activeHelper.attributes : null; } get unit() { return this.#activeHelper && this.#isEntity ? this.#activeHelper.unit : null; } get stateObj() { return this.#activeHelper && this.#isEntity ? this.#activeHelper.stateObj : null; } // ─── PUBLIC API METHODS ─────────────────────────────────────────────────── refresh() { if (this.#activeHelper && this.#isEntity) this.#activeHelper.refresh(); } } /****************************************************************************************** * 🛠️ Manage YAML options * ======================================================================================== * structural validation ideas to manage inputs (1.5+). * deliberately verbose by design: no external dependencies, fully typed errors, * and scales cleanly across multiple card types. * * @inspired by superstruct (MIT License) - Copyright (c) @ianstormtaylor * @see https://github.com/ianstormtaylor/superstruct */ class ValidationError extends Error { constructor(path = [], errorCode = null, severity = SEV.error, fallback = null, partialConfig = null, allErrors = []) { super(); this.name = 'ValidationError'; this.path = path; this.errorCode = errorCode; this.severity = severity; this.partialConfig = partialConfig; this.errors = allErrors; this.fallback = fallback; } } const SKIP_PROPERTY = Symbol('SKIP_PROPERTY'); const ERROR_CODES = { missingRequiredProperty: { code: 'missingRequiredProperty', severity: SEV.error }, invalidTypeString: { code: 'invalidTypeString', severity: SEV.error }, invalidTypeNumber: { code: 'invalidTypeNumber', severity: SEV.error }, invalidTypeBoolean: { code: 'invalidTypeBoolean', severity: SEV.error }, invalidTypeArray: { code: 'invalidTypeArray', severity: SEV.error }, invalidTypeObject: { code: 'invalidTypeObject', severity: SEV.error }, invalidEnumValue: { code: 'invalidEnumValue', severity: SEV.error }, invalidUnionType: { code: 'invalidUnionType', severity: SEV.error }, invalidEntityId: { code: 'invalidEntityId', severity: SEV.error }, invalidDecimal: { code: 'invalidDecimal', severity: SEV.error }, invalidActionObject: { code: 'invalidActionObject', severity: SEV.error }, missingActionKey: { code: 'missingActionKey', severity: SEV.error }, invalidCustomThemeArray: { code: 'invalidCustomThemeArray', severity: SEV.error }, invalidCustomThemeEntry: { code: 'invalidCustomThemeEntry', severity: SEV.error }, invalidMinValue: { code: 'invalidMinValue', severity: SEV.error }, invalidMaxValue: { code: 'invalidMaxValue', severity: SEV.error }, invalidTheme: { code: 'invalidTheme', severity: SEV.info }, minGreaterThanMax: { code: 'minGreaterThanMax', severity: SEV.error }, discontinuousRange: { code: 'discontinuousRange', severity: SEV.error }, missingColorProperty: { code: 'missingColorProperty', severity: SEV.error }, invalidIconType: { code: 'invalidIconType', severity: SEV.error }, invalidStateContent: { code: 'invalidStateContent', severity: SEV.error }, invalidStateContentEntry: { code: 'invalidStateContentEntry', severity: SEV.error }, appliedDefaultValue: { code: 'appliedDefaultValue', severity: SEV.info }, }; const validateType = (typeCheck, errorCode) => (value, path = []) => { if (is.nullish(value)) throw new ValidationError(path, ERROR_CODES.missingRequiredProperty.code, ERROR_CODES.missingRequiredProperty.severity); if (!typeCheck(value)) throw new ValidationError(path, errorCode.code, errorCode.severity); return value; }; const types = { string: validateType(is.string, ERROR_CODES.invalidTypeString), number: validateType(is.number, ERROR_CODES.invalidTypeNumber), boolean: validateType(is.boolean, ERROR_CODES.invalidTypeBoolean), array: (itemValidator) => (value, path = []) => { if (!is.array(value)) throw new ValidationError(path, ERROR_CODES.invalidTypeArray.code, ERROR_CODES.invalidTypeArray.severity); const validItems = []; value.forEach((item, index) => { const validatedItem = itemValidator(item, [...path, index]); if (validatedItem !== SKIP_PROPERTY) { validItems.push(validatedItem); } }); return validItems; }, object: (schema) => { const validator = (value, path = []) => { if (typeof value !== 'object' || value === null || Array.isArray(value)) { throw new ValidationError(path, ERROR_CODES.invalidTypeObject.code, ERROR_CODES.invalidTypeObject.severity); } const result = {}; const errors = []; for (const [key, fieldValidator] of Object.entries(schema)) { try { const validatedValue = fieldValidator(value[key], [...path, key]); if (validatedValue !== SKIP_PROPERTY) { result[key] = validatedValue; } } catch (error) { if (error instanceof ValidationError) { if (error.fallback !== null) { result[key] = error.fallback; } errors.push(error); } else { throw error; } } } if (errors.length > 0) { throw new ValidationError(errors[0].path, errors[0].errorCode, errors[0].severity, null, result, errors); } return result; }; validator._schema = schema; return validator; }, optional: (validator) => (value, path = []) => { if (is.nullish(value)) return SKIP_PROPERTY; try { return validator(value, path); } catch (error) { if (error instanceof ValidationError) { // Si c'est optional, on change la sévérité en INFO error.severity = SEV.info; } throw error; } }, fallbackTo: (validator, defaultVal) => (value, path = []) => { if (value === undefined) return defaultVal; try { return validator(value, path); } catch (error) { if (error instanceof ValidationError) { if (is.nullish(value)) { error.severity = SEV.info; error.errorCode = ERROR_CODES.appliedDefaultValue.code; } else { error.severity = SEV.warning; } error.fallback = defaultVal; } throw error; } }, optionalString: () => types.optional(types.string), optionalNumber: () => types.optional(types.number), optionalBoolean: () => types.optional(types.boolean), optionalWithDefault: (baseValidator, defaultVal) => types.fallbackTo(types.optional(baseValidator), defaultVal), optionalStringWithDefault: (defaultVal) => types.optionalWithDefault(types.string, defaultVal), optionalNumberWithDefault: (defaultVal) => types.optionalWithDefault(types.number, defaultVal), optionalBooleanWithDefault: (defaultVal) => types.optionalWithDefault(types.boolean, defaultVal), enums: (allowedValues) => (value, path = []) => { if (is.nullish(value)) { throw new ValidationError(path, ERROR_CODES.missingRequiredProperty.code, ERROR_CODES.missingRequiredProperty.severity); } if (!allowedValues.includes(value)) { throw new ValidationError(path, ERROR_CODES.invalidEnumValue.code, ERROR_CODES.invalidEnumValue.severity); } return value; }, enumsWithDefault: (allowedValues, defaultVal) => types.fallbackTo(types.enums(allowedValues), defaultVal), theme: (allowedValues) => (value, path = []) => { if (is.nullish(value) || is.emptyString(value)) return SKIP_PROPERTY; const themeMap = { battery: 'optimal_when_high', memory: 'optimal_when_low', cpu: 'optimal_when_low', }; value = themeMap[value] || value; if (!allowedValues.includes(value)) throw new ValidationError(path, ERROR_CODES.invalidTheme.code, ERROR_CODES.invalidTheme.severity); return value; }, union: (...validators) => (value, path = []) => { const errors = []; for (const validator of validators) { try { return validator(value, path); } catch (error) { errors.push(error.message || error.errorCode); } } throw new ValidationError(path, ERROR_CODES.invalidUnionType.code, ERROR_CODES.invalidUnionType.severity); }, arrayWithValidatedElem: (allowedValues) => // eslint-disable-next-line no-unused-vars (value, _path = []) => { if (is.nullish(value)) return SKIP_PROPERTY; const valueArray = is.array(value) ? value : [value]; const validItems = valueArray.filter((item) => allowedValues.includes(item)); if (validItems.length === 0) return SKIP_PROPERTY; return validItems; }, jinjaOrArrayWithValidatedElem: (allowedValues) => (value, path = []) => { if (is.jinja(value)) return value; return types.arrayWithValidatedElem(allowedValues)(value, path); }, watermarkObject: (schema) => (value, path = []) => { if (is.nullish(value) || !is.plainObject(value)) return SKIP_PROPERTY; const validateEntry = (key, validator) => { try { return { key, value: validator(value[key], [...path, key]), error: null }; } catch (error) { if (!(error instanceof ValidationError)) throw error; return { key, value: error.fallback ?? undefined, error }; } }; const results = Object.entries(schema).map(([key, validator]) => validateEntry(key, validator)); const errors = results.filter((r) => r.error).map((r) => r.error); const result = Object.fromEntries( results .filter((r) => r.value !== SKIP_PROPERTY && r.value !== undefined) .map((r) => [r.key, r.value]) ); if (errors.length > 0) { throw new ValidationError(path, 'watermarkValidation', SEV.warning, result, null, errors); } return result; }, entityId: (value, path = []) => { if (is.nullish(value)) throw new ValidationError(path, ERROR_CODES.missingRequiredProperty.code, ERROR_CODES.missingRequiredProperty.severity); if (typeof value !== 'string') throw new ValidationError(path, ERROR_CODES.invalidTypeString.code, ERROR_CODES.invalidTypeString.severity); if (!/^[a-z_]+\.[a-z0-9_]+$/.test(value)) throw new ValidationError(path, ERROR_CODES.invalidEntityId.code, ERROR_CODES.invalidEntityId.severity); return value; }, decimal: (value, path = []) => { if (is.nullish(value)) return SKIP_PROPERTY; if (!is.integer(value)) throw new ValidationError(path, ERROR_CODES.invalidDecimal.code, ERROR_CODES.invalidDecimal.severity); return value; }, tapAction: (value, path = []) => { if (typeof value !== 'object' || value === null || Array.isArray(value)) { throw new ValidationError(path, ERROR_CODES.invalidActionObject.code, ERROR_CODES.invalidActionObject.severity); } if (!is.string(value.action)) { throw new ValidationError([...path, 'action'], ERROR_CODES.missingActionKey.code, ERROR_CODES.missingActionKey.severity); } return value; }, tapActionWithDefault: (defaultVal) => types.fallbackTo(types.tapAction, defaultVal), customTheme: (value, path = []) => { if (is.nullish(value)) return SKIP_PROPERTY; if (!is.array(value)) throw new ValidationError(path, ERROR_CODES.invalidCustomThemeArray.code, ERROR_CODES.invalidCustomThemeArray.severity); let previousMax = null; return value.map((item, index) => { const itemPath = [...path, index]; if (!is.plainObject(item)) throw new ValidationError(itemPath, ERROR_CODES.invalidCustomThemeEntry.code, ERROR_CODES.invalidCustomThemeEntry.severity); const { min, max, color, icon_color, bar_color, icon } = item; if (!is.number(min)) throw new ValidationError([...itemPath, 'min'], ERROR_CODES.invalidMinValue.code, ERROR_CODES.invalidMinValue.severity); if (!is.number(max)) throw new ValidationError([...itemPath, 'max'], ERROR_CODES.invalidMaxValue.code, ERROR_CODES.invalidMaxValue.severity); if (min >= max) throw new ValidationError(itemPath, ERROR_CODES.minGreaterThanMax.code, ERROR_CODES.minGreaterThanMax.severity); if (previousMax !== null && min !== previousMax) { throw new ValidationError([...itemPath, 'min'], ERROR_CODES.discontinuousRange.code, ERROR_CODES.discontinuousRange.severity); } const hasColor = is.string(color) || is.string(icon_color) || is.string(bar_color); if (!hasColor) { throw new ValidationError(itemPath, ERROR_CODES.missingColorProperty.code, ERROR_CODES.missingColorProperty.severity); } if (icon !== undefined && !is.string(icon)) { throw new ValidationError([...itemPath, 'icon'], ERROR_CODES.invalidIconType.code, ERROR_CODES.invalidIconType.severity); } previousMax = max; return { min, max, ...(color !== undefined && { color }), ...(icon_color !== undefined && { icon_color }), ...(bar_color !== undefined && { bar_color }), ...(icon !== undefined && { icon }), }; }); }, stateContent: (value, path = []) => { if (is.nullishOrEmptyString(value)) return SKIP_PROPERTY; if (is.string(value)) return [value]; if (is.array(value)) { const invalidIndex = value.findIndex((v) => typeof v !== 'string'); if (invalidIndex !== -1) { throw new ValidationError([...path, invalidIndex], ERROR_CODES.invalidStateContentEntry.code, ERROR_CODES.invalidStateContentEntry.severity); } return value; } throw new ValidationError(path, ERROR_CODES.invalidStateContent.code, ERROR_CODES.invalidStateContent.severity); }, }; function struct(validator) { const preProcess = (data) => { const result = { ...data }; if (!data.type.includes('template')) { if (is.nonEmptyString(result.name)) { result.name = [{ type: 'text', text: result.name }]; } else if (is.plainObject(result.name)) { result.name = [result.name]; } } if (is.nullish(result.icon_tap_action) && is.string(result.entity)) { const domain = HassProviderSingleton.getEntityDomain(result.entity); const shouldPatch = HA_CONTEXT.actions.toggleDomain.includes(domain); if (shouldPatch) result.icon_tap_action = HA_CONTEXT.actions.toggle; } if (['top', 'bottom', 'overlay', 'background'].includes(result.bar_position)) delete result.bar_size; // avoid conflict return result; }; const postProcess = (data) => { const result = { ...data }; if (!result.layout) result.layout = CARD.layout.orientations.horizontal.label; if (result.bar_size === CARD.style.bar.sizeOptions.xlarge.label && result.bar_position === 'default') result.bar_position = 'below'; if (result.bar_position !== 'overlay' && result.bar_single_line) result.bar_single_line = false; return result; }; return { validate: (data) => { try { const preProcessed = preProcess(data); return { isValid: true, config: postProcess(validator(preProcessed)), error: null, path: null }; } catch (error) { return { isValid: false, config: null, error: error.message, path: error.path }; } }, parse: (data) => { try { const preProcessed = preProcess(data); const result = postProcess(validator(preProcessed)); return { isValid: true, config: result, path: null, errorCode: null, severity: null, errors: [], }; } catch (error) { // extract error wo duplicates const extractAllErrors = (errRoot) => { const allErrors = []; const seen = new Set(); const addError = (err) => { const key = `${JSON.stringify(err.path)}-${err.errorCode}`; if (!seen.has(key)) { seen.add(key); allErrors.push({ path: err.path, errorCode: err.errorCode, severity: err.severity, }); } }; if (errRoot.errors && Array.isArray(errRoot.errors) && errRoot.errors.length > 0) { errRoot.errors.forEach((subError) => { if (subError instanceof ValidationError) { extractAllErrors(subError).forEach(addError); } else if (subError.errorCode) { addError(subError); } }); } else if (errRoot.errorCode) { addError(errRoot); } return allErrors; }; const allErrors = extractAllErrors(error); const mainError = allErrors.find((e) => e.severity === 'error') || allErrors[0] || null; const partialConfig = error.partialResult ?? error.partialConfig ?? null; const postProcessedPartialConfig = partialConfig !== null ? postProcess(partialConfig) : null; return { isValid: !mainError || mainError.severity !== 'error', config: postProcessedPartialConfig, path: mainError?.path ?? null, errorCode: mainError?.errorCode ?? null, severity: mainError?.severity ?? null, errors: allErrors, }; } }, extend: (additionalFields) => { if (!validator._schema) { throw new Error('Can only extend object schemas created with types.object'); } const newSchema = { ...validator._schema, ...additionalFields, }; return struct(types.object(newSchema)); }, delete: (fieldsToDelete) => { if (!validator._schema) { throw new Error('Can only delete from object schemas created with types.object'); } const fieldsArray = Array.isArray(fieldsToDelete) ? fieldsToDelete : [fieldsToDelete]; const newSchema = { ...validator._schema }; fieldsArray.forEach((field) => { delete newSchema[field]; }); return struct(types.object(newSchema)); }, fields: () => { if (!validator._schema) { throw new Error('Can only get fields from object schemas created with types.object'); } return Object.keys(validator._schema); }, }; } types.discriminatedUnion = (key, mapping) => (value, path = []) => { if (value === null || typeof value !== 'object' || Array.isArray(value)) { throw new ValidationError(path, ERROR_CODES.invalidTypeObject.code, ERROR_CODES.invalidTypeObject.severity); } const discriminator = value[key]; if (typeof discriminator !== 'string') { throw new ValidationError( [...path, key], ERROR_CODES.invalidTypeString.code, ERROR_CODES.invalidTypeString.severity ); } const validator = mapping[discriminator]; if (!validator) { throw new ValidationError( [...path, key], ERROR_CODES.invalidEnumValue.code, ERROR_CODES.invalidEnumValue.severity ); } return validator(value, path); }; const nameItem = types.discriminatedUnion('type', { text: types.object({ type: types.enums(['text']), text: types.string, }), entity: types.object({ type: types.enums(['entity']), }), device: types.object({ type: types.enums(['device']), }), area: types.object({ type: types.enums(['area']), }), floor: types.object({ type: types.enums(['floor']), }), }); types.name = types.array(nameItem); const additionItem = types.fallbackTo( types.object({ entity: types.entityId, attribute: types.optional(types.string), }), SKIP_PROPERTY, ); const watermarkSchema = { low: types.fallbackTo(types.union(types.number, types.string), CARD.config.defaults.watermark.low), low_attribute: types.optionalString(), low_color: types.optionalStringWithDefault(CARD.config.defaults.watermark.low_color), high: types.fallbackTo(types.union(types.number, types.string), CARD.config.defaults.watermark.high), high_attribute: types.optionalString(), high_color: types.optionalStringWithDefault(CARD.config.defaults.watermark.high_color), opacity: types.optionalNumberWithDefault(CARD.config.defaults.watermark.opacity), type: types.enumsWithDefault(['blended', 'area', 'striped', 'triangle', 'round', 'line'], CARD.config.defaults.watermark.type), line_size: types.optionalStringWithDefault(CARD.config.defaults.watermark.line_size), disable_low: types.optionalBooleanWithDefault(CARD.config.defaults.watermark.disable_low), disable_high: types.optionalBooleanWithDefault(CARD.config.defaults.watermark.disable_high), }; class YamlSchemaFactory { static get feature() { return struct( types.object({ // ─── Entity & Data ────────────────────────────────────────────────── entity: types.entityId, attribute: types.optionalString(), min_value: types.optionalNumber(0), max_value: types.fallbackTo(types.union(types.number, types.string), 100), max_value_attribute: types.optionalString(), // ─── Appearance ───────────────────────────────────────────────────── bar_color: types.optionalString(), bar_size: types.enumsWithDefault( Object.values(CARD.style.bar.sizeOptions).map((e) => e.label), 'small', ), //[('small', 'medium', 'large', 'xlarge')] bar_orientation: types.enumsWithDefault(Object.keys(CARD.style.dynamic.progressBar.orientation), 'ltr'), // ['ltr', 'rtl'] bar_effect: types.jinjaOrArrayWithValidatedElem(Object.values(CARD.style.dynamic.progressBar.effect).map((e) => e.label)), //[('radius', 'glass', 'gradient', 'shimmer')] bar_position: types.enumsWithDefault(['default', 'top', 'bottom'], 'default'), center_zero: types.optionalBooleanWithDefault(false), // ─── Theme & Watermark ────────────────────────────────────────────── theme: types.theme(Object.keys(THEME)), custom_theme: types.fallbackTo(types.customTheme, SKIP_PROPERTY), interpolate: types.optionalBooleanWithDefault(false), watermark: types.watermarkObject(watermarkSchema, CARD.config.defaults.watermark), // ─── Additions ────────────────────────────────────────────────────── additions: types.optional(types.array(additionItem)), }), ); } static get card() { return struct( types.object({ // ─── Entity & Data ────────────────────────────────────────────────── entity: types.entityId, attribute: types.optionalString(), name: types.optional(types.name), decimal: types.decimal, unit: types.optionalString(), disable_unit: types.optionalBooleanWithDefault(false), unit_spacing: types.enumsWithDefault(Object.values(CARD.config.unit.unitSpacing), 'auto'), //['auto', 'space', 'no-space'] min_value: types.optionalNumber(0), max_value: types.fallbackTo(types.union(types.number, types.string), 100), max_value_attribute: types.optionalString(), // ─── Appearance === icon: types.optionalString(), color: types.optionalString(), bar_color: types.optionalString(), bar_size: types.enumsWithDefault( Object.values(CARD.style.bar.sizeOptions).map((e) => e.label), 'small', ), //[('small', 'medium', 'large', 'xlarge')] bar_orientation: types.enumsWithDefault(Object.keys(CARD.style.dynamic.progressBar.orientation), 'ltr'), // ['ltr', 'rtl'] bar_effect: types.jinjaOrArrayWithValidatedElem(Object.values(CARD.style.dynamic.progressBar.effect).map((e) => e.label)), //[('radius', 'glass', 'gradient', 'shimmer')] bar_position: types.enumsWithDefault(['default', 'below', 'top', 'bottom', 'overlay', 'background'], 'default'), bar_single_line: types.optionalBooleanWithDefault(false), bar_max_width: types.optionalString(), layout: types.enumsWithDefault( Object.values(CARD.layout.orientations).map((e) => e.label), 'horizontal', ), // [('horizontal', 'vertical')] min_width: types.optionalString(), height: types.optionalString(), frameless: types.optionalBooleanWithDefault(false), marginless: types.optionalBooleanWithDefault(false), reverse: types.optionalBooleanWithDefault(false), reverse_secondary_info_row: types.optionalBooleanWithDefault(false), force_circular_background: types.optionalBooleanWithDefault(false), center_zero: types.optionalBooleanWithDefault(false), trend_indicator: types.optionalBooleanWithDefault(false), text_shadow: types.optionalBooleanWithDefault(false), // ─── Visibility & Content === hide: types.jinjaOrArrayWithValidatedElem(['icon', 'name', 'value', 'secondary_info', 'progress_bar']), name_info: types.optionalString(), custom_info: types.optionalString(), state_content: types.optional(types.fallbackTo(types.stateContent, SKIP_PROPERTY)), // ─── Badges === badge_icon: types.optionalString(), badge_color: types.optionalString(), // ─── Theme & Watermark === // theme: types.theme(['optimal_when_low', 'optimal_when_high', 'light', 'temperature', 'humidity', 'pm25', 'voc']), theme: types.theme(Object.keys(THEME)), custom_theme: types.fallbackTo(types.customTheme, SKIP_PROPERTY), interpolate: types.optionalBooleanWithDefault(false), watermark: types.watermarkObject(watermarkSchema, CARD.config.defaults.watermark), // ─── Additions === additions: types.optional(types.array(additionItem)), // ─── Actions === tap_action: types.tapActionWithDefault(HA_CONTEXT.actions.moreInfo), hold_action: types.tapActionWithDefault(HA_CONTEXT.actions.none), double_tap_action: types.tapActionWithDefault(HA_CONTEXT.actions.none), icon_tap_action: types.tapActionWithDefault(HA_CONTEXT.actions.none), icon_hold_action: types.tapActionWithDefault(HA_CONTEXT.actions.none), icon_double_tap_action: types.tapActionWithDefault(HA_CONTEXT.actions.none), }), ); } static get badge() { return YamlSchemaFactory.card.delete([ 'bar_position', 'badge_icon', 'badge_color', 'force_circular_background', 'layout', 'height', 'icon_tap_action', 'icon_hold_action', 'icon_double_tap_action', ]); } static get template() { return struct( types.object({ // ─── Entity & Data === entity: types.optional(types.entityId), name: types.optionalString(), secondary: types.optionalString(), percent: types.optionalString(), // ─── Appearance === icon: types.optionalString(), color: types.optionalString(), bar_color: types.optionalString(), bar_size: types.enumsWithDefault( Object.values(CARD.style.bar.sizeOptions).map((e) => e.label), 'small', ), //[('small', 'medium', 'large', 'xlarge')] bar_orientation: types.enumsWithDefault(Object.keys(CARD.style.dynamic.progressBar.orientation), 'ltr'), // ['ltr', 'rtl'] bar_effect: types.jinjaOrArrayWithValidatedElem(Object.values(CARD.style.dynamic.progressBar.effect).map((e) => e.label)), //[('radius', 'glass', 'gradient', 'shimmer')] bar_position: types.enumsWithDefault(['default', 'below', 'top', 'bottom', 'overlay', 'background'], 'default'), bar_single_line: types.optionalBooleanWithDefault(false), bar_max_width: types.optionalString(), layout: types.enumsWithDefault( Object.values(CARD.layout.orientations).map((e) => e.label), 'horizontal', ), // [('horizontal', 'vertical')] min_width: types.optionalString(), height: types.optionalString(), frameless: types.optionalBooleanWithDefault(false), marginless: types.optionalBooleanWithDefault(false), reverse_secondary_info_row: types.optionalBooleanWithDefault(false), force_circular_background: types.optionalBooleanWithDefault(false), center_zero: types.optionalBooleanWithDefault(false), trend_indicator: types.optionalBooleanWithDefault(false), text_shadow: types.optionalBooleanWithDefault(false), hide: types.jinjaOrArrayWithValidatedElem(['icon', 'name', 'value', 'secondary_info', 'progress_bar']), badge_icon: types.optionalString(), badge_color: types.optionalString(), watermark: types.watermarkObject(watermarkSchema, CARD.config.defaults.watermark), // ─── Actions === tap_action: types.tapActionWithDefault(HA_CONTEXT.actions.moreInfo), hold_action: types.tapActionWithDefault(HA_CONTEXT.actions.none), double_tap_action: types.tapActionWithDefault(HA_CONTEXT.actions.none), icon_tap_action: types.tapActionWithDefault(HA_CONTEXT.actions.none), icon_hold_action: types.tapActionWithDefault(HA_CONTEXT.actions.none), icon_double_tap_action: types.tapActionWithDefault(HA_CONTEXT.actions.none), }), ); } static get badgeTemplate() { return YamlSchemaFactory.template.delete([ 'bar_position', 'badge_icon', 'badge_color', 'force_circular_background', 'layout', 'height', 'icon_tap_action', 'icon_hold_action', 'icon_double_tap_action', ]); } } /****************************************************************************************** * 🛠️ BaseConfigHelper * ======================================================================================== * * ✅ base class for managing and validating all card configuration. * * @class */ class BaseConfigHelper { #hassProvider = HassProviderSingleton.getInstance(); #HAError = null; #lastMsgConsole = null; #log = null; #actions = { card: { tap: null, doubleTap: null, hold: null }, icon: { tap: null, doubleTap: null, hold: null }, }; #actionsReady = false; _isDefined = false; _configParsed = {}; _yamlSchema = null; constructor() { this.#log = initLogger(this, false); } // ─── PUBLIC GETTERS / SETTERS ───────────────────────────────────────────── get config() { return this._configParsed?.config; } set config(config) { this.#actionsReady = false; this._isDefined = true; BaseConfigHelper.#logDeprecatedOption(config); this._configParsed = this._yamlSchema.parse(this.constructor._customizeConfig(config)); this.#lastMsgConsole = null; } static _customizeConfig(config) { return config; } static #logDeprecatedOption(config) { if (config.navigate_to !== undefined) console.warn(`${META.types.card.typeName.toUpperCase()} - navigate_to option is deprecated and has been removed.`); if (config.show_more_info !== undefined) console.warn(`${META.types.card.typeName.toUpperCase()} - show_more_info option is deprecated and has been removed.`); if (['battery', 'cpu', 'memory'].includes(config.theme)) console.warn( `${META.types.card.typeName.toUpperCase()} - theme: ${ config.theme } is deprecated and will be removed in a future release. Please migrate to the recommended alternative...`, ); } get isValid() { return this._isDefined ? this._configParsed.isValid && this.#HAError === null : false; } get _errorMessage() { const errorSrc = this.#HAError ? this.#HAError : this._configParsed; return { content: `${errorSrc.path}: ${this.#hassProvider.getMessage(errorSrc.errorCode)}`, sev: errorSrc.severity, }; } get msg() { return this._isDefined && (this._configParsed.errorCode || this.#HAError) ? this._errorMessage : null; } get hasDisabledUnit() { return this.config?.disable_unit; } get action() { if (!this.#actionsReady) { this.#actions = { card: { tap: this.#getAction('tap_action'), doubleTap: this.#getAction('double_tap_action'), hold: this.#getAction('hold_action'), }, icon: { tap: this.#getAction('icon_tap_action'), doubleTap: this.#getAction('icon_double_tap_action'), hold: this.#getAction('icon_hold_action'), }, }; this.#actionsReady = true; } return this.#actions; } #getAction(action) { return this.isValid ? this.config?.[action]?.action : null; } checkConfig() { this._showConfigErrorConsole(); // structure, type... this._checkHAEnvironment(); // ha env: entity, attribute ... } _showConfigErrorConsole() { if (is.nonEmptyArray(this._configParsed.errors)) { const curError = this._configParsed.errors[0]; const msgConsole = `${curError.path.join('.')} : ${this._hassProvider.getMessage(curError.errorCode)}`; if (this.#lastMsgConsole !== msgConsole) { this.#lastMsgConsole = msgConsole; this.#log[curError.severity]?.(msgConsole); this.#log[curError.severity]?.('config: ', this.config); } } } _checkHAEnvironment() { const resolve = (key) => (is.nonEmptyString(key) ? this._hassProvider.getEntityStateObj(key) : null); const entityState = resolve(this.config.entity); const maxValueState = resolve(this.config.max_value); const lowWMState = resolve(this.config?.watermark?.low); const highWMState = resolve(this.config?.watermark?.high); const checks = [ { condition: is.string(this.config.attribute) && entityState && !has.own(entityState.attributes, this.config.attribute), path: 'attribute', errorCode: 'attributeNotFound', }, { condition: is.nonEmptyString(this.config.max_value) && !maxValueState, path: 'max_value', errorCode: 'entityNotFound' }, { condition: is.nonEmptyString(this.config.max_value_attribute) && maxValueState && !has.own(maxValueState.attributes, this.config.max_value_attribute), path: 'max_value_attribute', errorCode: 'attributeNotFound', }, { condition: is.nonEmptyString(this.config.watermark?.low) && !lowWMState, path: 'watermark.low', errorCode: 'entityNotFound' }, { condition: is.nonEmptyString(this.config.watermark?.low_attribute) && lowWMState && !has.own(lowWMState.attributes, this.config.watermark.low_attribute), path: 'watermark.low_attribute', errorCode: 'attributeNotFound', }, { condition: is.nonEmptyString(this.config.watermark?.high) && !highWMState, path: 'watermark.high', errorCode: 'entityNotFound' }, { condition: is.nonEmptyString(this.config.watermark?.high_attribute) && highWMState && !has.own(highWMState.attributes, this.config.watermark.high_attribute), path: 'watermark.high_attribute', errorCode: 'attributeNotFound', }, ]; const failed = checks.find((c) => c.condition); this.#HAError = failed ? { path: failed.path, errorCode: failed.errorCode, severity: SEV.error } : null; } get _hassProvider() { return this.#hassProvider; } } class CardConfigHelper extends BaseConfigHelper { _yamlSchema = YamlSchemaFactory.card; static _customizeConfig(config) { return { ...config, ...(is.nonEmptyString(config?.entity) && is.nullish(config?.attribute) ? { attribute: HA_CONTEXT.attributeMapping[HassProviderSingleton.getEntityDomain(config?.entity)]?.attribute } : {}), ...(is.nonEmptyString(config?.max_value) && is.nullish(config?.max_value_attribute) ? { max_value_attribute: HA_CONTEXT.attributeMapping[HassProviderSingleton.getEntityDomain(config?.max_value)]?.attribute } : {}), }; } get max_value() { if (!this.config.max_value) return CARD.config.value.max; if (Number.isFinite(this.config.max_value)) return this.config.max_value; if (is.string(this.config.max_value)) { const state = this._hassProvider.getEntityProp(this.config.max_value, 'state'); const parsedState = parseFloat(state); if (!isNaN(parsedState)) return parsedState; } return null; } get stateContent() { return this.config?.state_content ?? []; } } class BadgeConfigHelper extends CardConfigHelper { _yamlSchema = YamlSchemaFactory.badge; } class FeatureConfigHelper extends CardConfigHelper { _yamlSchema = YamlSchemaFactory.feature; } class TemplateConfigHelper extends BaseConfigHelper { _yamlSchema = YamlSchemaFactory.template; } class BadgeTemplateConfigHelper extends BaseConfigHelper { _yamlSchema = YamlSchemaFactory.badgeTemplate; } /****************************************************************************************** * 🛠️ ViewCore * ======================================================================================== * * ✅ A view class for rendering minimal cards in a user interface. * This class manages configuration, entity states, user interactions, and visual * appearance of cards including layouts, orientations, watermarks, and interactive elements. * * ViewCore * ├── ViewBase * │ ├── CardView * │ ├── BadgeView * │ └── FeatureView * ├── CardTemplateView * └── BadgeTemplateView * * @class * @description Handles the display and behavior of minimal cards with support for * Home Assistant entities, user actions, and visual customization * (watermarks, shapes, orientations, clickable elements). * * @example * const cardView = new ViewCore(); * cardView.config = { * entity: 'sensor.temperature', * layout: 'vertical', * bar_orientation: 'rtl', * force_circular_background: true, * watermark: { low: 10, high: 30, type: 'gradient' } * }; * * // Check if components are hidden * if (!cardView.hasComponentHiddenFlag('icon')) { * // Render icon * } * * // Access computed properties * const hasShape = cardView.hasVisibleShape; * const isClickable = cardView.hasClickableCard; */ class ViewCore { _hassProvider = HassProviderSingleton.getInstance(); _lastPercent = null; _configHelper = new BaseConfigHelper(); // Base config _currentValue = new EntityOrValue(); _lowValue = new EntityOrValue(); _highValue = new EntityOrValue(); // ─── PUBLIC GETTERS / SETTERS ───────────────────────────────────────────── set config(config) { if (!config) { throw new Error(CARD.config.configError); } this._configHelper.config = config; console.log("DEBUG CONFIG: ", { config, newConfig: this._configHelper.config }); Object.assign(this._currentValue, { value: this._configHelper.config.entity, stateContent: this._configHelper.stateContent, }); Object.assign(this._lowValue, { value: this._configHelper.config?.watermark?.low, attribute: this._configHelper.config?.watermark?.low_attribute, }); Object.assign(this._highValue, { value: this._configHelper.config?.watermark?.high, attribute: this._configHelper.config?.watermark?.high_attribute, }); } get config() { return this._configHelper?.config; } refresh(hass) { this._hassProvider.hass = hass; this._currentValue.refresh(); this._lowValue.refresh(); this._highValue.refresh(); } get entity() { return this.config ? this.config.entity : undefined; } get cardSize() { return this.config ? (CARD.layout.orientations[this.config.layout]?.grid?.grid_rows ?? 1) : CARD.layout.orientations.horizontal.grid.grid_rows; } get cardLayoutOptions() { if (!this.config) return CARD.layout.orientations.horizontal.grid; const layout = structuredClone(CARD.layout.orientations[this.config.layout]); layout.grid.grid_min_rows = this.hasComponentHiddenFlag(CARD.style.dynamic.hiddenComponent.icon.label) ? 1 : layout.grid.grid_min_rows + (this.config.bar_size === CARD.style.bar.sizeOptions.xlarge.label || this.config.layout === 'horizontal' && this.config.bar_position === 'below' || (this.config.layout === 'vertical' && ['default', 'below'].includes(this.config.bar_position) && this.config.bar_size !== 'small') ? 1 : 0); return layout.grid; } _getEntityColor() { if (this._currentValue.state === HA_CONTEXT.entity.state.unavailable) return CARD.style.color.unavailable; if (this._currentValue.state === HA_CONTEXT.entity.state.notFound) return CARD.style.color.notFound; return ThemeManager.adaptColor(this._currentValue.defaultColor || CARD.style.color.default); } get barColor() { return this.entity && !this._configHelper.config.bar_color ? this._getEntityColor() : null; } get iconColor() { return this.entity && !this._configHelper.config.color ? this._getEntityColor() : null; } get hasClickableIcon() { return ViewCore.#hasAction([this._configHelper.action.icon.tap, this._configHelper.action.icon.hold, this._configHelper.action.icon.doubleTap]); } get hasClickableCard() { return ViewCore.#hasAction([this._configHelper.action.card.tap, this._configHelper.action.card.hold, this._configHelper.action.card.doubleTap]); } get hasReversedSecondaryInfoRow() { return this.config.layout === 'horizontal' && this.config.bar_position === 'default' && this.config.reverse_secondary_info_row; // ─── true } get hasVisibleShape() { return this.config.force_circular_background || this._hasDefaultShape || this._hasInteractiveShape; // this.config.force_circular_background === true } get _hasDefaultShape() { return this._currentValue.hasShapeByDefault && ViewCore.#hasAction([this._configHelper.action.icon.tap]); } get _hasInteractiveShape() { return this._configHelper.action.icon.tap !== HA_CONTEXT.actions.none.action; } get hasWatermark() { return this.config.watermark !== undefined; } get barEffectsEnabled() { return this.config.bar_effect !== undefined; } get watermark() { const { watermark } = this.config; return watermark ? { ...watermark, low: this._lowValue.value, low_color: ThemeManager.adaptColor(watermark.low_color), high: this._highValue.value, high_color: ThemeManager.adaptColor(watermark.high_color), } : null; } // ─── PUBLIC API METHODS ─────────────────────────────────────────────────── getTrend(currentPercent) { const result = this._lastPercent === null ? 'flat' : this._lastPercent < currentPercent ? 'up' : this._lastPercent > currentPercent ? 'down' : 'flat'; this._lastPercent = currentPercent; return result; } hasComponentHiddenFlag(component) { return this._hasInConfigArray('hide', component); } hasBarEffect(component) { return this._hasInConfigArray('bar_effect', component); } // ─── PRIVATE METHODS ────────────────────────────────────────────────────── _hasInConfigArray(key, value) { return is.array(this.config?.[key]) && this.config[key].includes(value); } static #hasAction(actions) { return actions.some((action) => action !== HA_CONTEXT.actions.none.action); } } /****************************************************************************************** * 🛠️ ViewBase * ======================================================================================== * * ✅ A comprehensive base card view that extends ViewCore to manage all information * required for creating cards and badges. This class handles entity states, theme management, * percentage calculations, timers, and provides a complete API for card rendering. * * ViewCore * │ * ├── ViewBase * │ ├── CardView (CardConfigHelper) * │ ├── BadgeView (BadgeConfigHelper) * │ └── FeatureView (FeatureConfigHelper) * │ * └── (direct) * ├── CardTemplateView (TemplateConfigHelper) * └── BadgeTemplateView (BadgeTemplateConfigHelper) * * @class * @extends ViewCore * @description Manages the complete lifecycle of card display including: * - Entity state management and validation * - Theme and color management * - Percentage and progress calculations * - Timer and counter handling * - Badge and watermark rendering * - Multi-language support * - Error state handling (unavailable, not found, unknown) * * @example * const cardView = new ViewBase(); * cardView.config = { * entity: 'sensor.cpu_percent', * name: 'CPU Usage', * max_value: 100, * unit: '%', * color: '#ff6b6b', * watermark: { low: 30, high: 80, type: 'gradient' } * }; * * // Refresh with Home Assistant data * cardView.refresh(hass); * * // Access computed properties * const isReady = cardView.isAvailable; * const progress = cardView.percent; * const displayText = cardView.secondaryInfoMain; * const cardColor = cardView.iconColor; * * // Handle error states * if (cardView.hasStandardEntityError) { * console.log('Entity has errors:', cardView.msg); * } * * // Timer-specific usage * if (cardView.isActiveTimer) { * const speed = cardView.refreshSpeed; * // Update UI at calculated refresh rate * } */ class ViewBase extends ViewCore { #percentHelper = new PercentHelper(); #theme = new ThemeManager(); #maxValue = new EntityOrValue(); #entityCollection = new EntityCollectionHelper(); // ─── PUBLIC GETTERS / SETTERS ───────────────────────────────────────────── get hasValidatedConfig() { return this._configHelper.isValid; } get msg() { return this._configHelper.msg; } set config(config) { if (!config) { throw new Error(CARD.config.configError); } this._configHelper.config = config; if (this._configHelper.config.additions) { this._configHelper.config.additions.forEach(({ entity, attribute }) => { this.#entityCollection.addEntity(entity, attribute); }); this.#entityCollection.addEntity(this._configHelper.config.entity, this._configHelper.config.attribute); } Object.assign(this.#percentHelper, { unitSpacing: this._configHelper.config.unit_spacing, hasDisabledUnit: this._configHelper.hasDisabledUnit, isCenterZero: this._configHelper.config.center_zero, }); Object.assign(this.#theme, { theme: this._configHelper.config.theme, customTheme: this._configHelper.config.custom_theme, interpolate: this._configHelper.config.interpolate, }); Object.assign(this._currentValue, { value: this._configHelper.config.entity, nameTokens: this._configHelper.config.name, stateContent: this._configHelper.stateContent, }); if (this._currentValue.entityType.isTimer) { this.#maxValue.value = CARD.config.value.max; } else { this._currentValue.attribute = this._configHelper.config.attribute; Object.assign(this.#maxValue, { value: this._configHelper.config.max_value ?? CARD.config.value.max, attribute: this._configHelper.config.max_value_attribute, }); Object.assign(this._lowValue, { value: this._configHelper.config?.watermark?.low, attribute: this._configHelper.config?.watermark?.low_attribute, }); Object.assign(this._highValue, { value: this._configHelper.config?.watermark?.high, attribute: this._configHelper.config?.watermark?.high_attribute, }); } } get config() { return this._configHelper.config; } #hasState(state) { const toEVal = this.hasWatermark ? [this._currentValue, this.#maxValue, this._lowValue, this._highValue] : [this._currentValue, this.#maxValue]; return toEVal.some((v) => v.state === state); } get isUnknown() { return this.#hasState(HA_CONTEXT.entity.state.unknown); } get isUnavailable() { return this.#hasState(HA_CONTEXT.entity.state.unavailable); } get isNotFound() { return this.#hasState(HA_CONTEXT.entity.state.notFound); } get isAvailable() { return !( !this._currentValue.isAvailable || (!this.#maxValue.isAvailable && this._configHelper.maxValue) || (!this._lowValue.isAvailable && this._configHelper.config?.watermark?.low) || (!this._highValue.isAvailable && this._configHelper.config?.watermark?.high) ); } get hasStandardEntityError() { return this.isUnavailable || this.isNotFound || this.isUnknown; } // ─── Getters for card ───────────────────────────────────────────────────── get icon() { const notFound = this.isNotFound ? CARD.style.icon.notFound.icon : null; return notFound || this.#theme.icon || this._configHelper.config.icon; } get iconColor() { if (this.isUnavailable) return CARD.style.color.unavailable; if (this.isNotFound) return CARD.style.color.notFound; return ( ThemeManager.adaptColor(this.#theme.iconColor || this._configHelper.config.color) || this._currentValue.defaultColor || CARD.style.color.default ); } get barColor() { if (!this.isAvailable) return this.isUnknown ? CARD.style.color.default : CARD.style.color.disabled; const curColor = ThemeManager.adaptColor(this.#theme.barColor || this._configHelper.config.bar_color) || this._currentValue.defaultColor || CARD.style.color.default; return this.hasEntityCollection ? this.#entityCollection.getEntitiesColor(curColor) : curColor; } get percent() { if (!this.isAvailable) return 0; return this.#percentHelper.isCenterZero ? Math.max(-100, Math.min(100, this.#percentHelper.percent)) : Math.max(0, Math.min(100, this.#percentHelper.percent)); } getTrend() { return super.getTrend(this.#percentHelper.percent); } get secondaryInfoMain() { if (this.hasStandardEntityError || (this._currentValue.entityType.isTimer && this._currentValue.value.state === HA_CONTEXT.entity.state.idle)) return this._currentValue.formatedEntityState; const additionalInfo = this._currentValue.stateContentToString; if (this.hasComponentHiddenFlag(CARD.style.dynamic.hiddenComponent.value.label)) return additionalInfo; const valueInfo = this._currentValue.entityType.isDuration && !this._configHelper.config.unit ? this._currentValue.formatedEntityState : this.#percentHelper.toString(); return additionalInfo === '' ? valueInfo : [additionalInfo, valueInfo].join(CARD.config.separator); } get name() { return is.nonEmptyArray(this._configHelper.config.name) ? this._currentValue.nameComposition : this._configHelper.config.name || this._currentValue.name || this._configHelper.config.entity; } get badgeInfo() { if (this.isNotFound) return CARD.style.icon.badge.notFound; if (this.isUnavailable) return CARD.style.icon.badge.unavailable; if (this._currentValue.entityType.isTimer) { const { state } = this._currentValue.value; const { paused, active } = HA_CONTEXT.entity.state; if (state === paused) return CARD.style.icon.badge.timer.paused; if (state === active) return CARD.style.icon.badge.timer.active; } return null; } get isActiveTimer() { return this._currentValue.entityType.isTimer && this._currentValue.state === HA_CONTEXT.entity.state.active; } get refreshSpeed() { const rawSpeed = this._currentValue.value.duration / CARD.config.refresh.ratio; const clampedSpeed = Math.min(CARD.config.refresh.max, Math.max(CARD.config.refresh.min, rawSpeed)); return Math.max(100, Math.round(clampedSpeed / 100) * 100); } get hasVisibleShape() { return this._hassProvider.hasNewShapeStrategy ? super.hasVisibleShape : true; } get timerIsReversed() { return this._configHelper.config.reverse !== false && this._currentValue.value.state !== HA_CONTEXT.entity.state.idle; } get hasWatermark() { return this._configHelper.config.watermark !== undefined; } get watermark() { const { watermark } = this.config; return watermark ? { ...watermark, low: this.#percentHelper.calcWatermark(this._lowValue.value), low_color: ThemeManager.adaptColor(watermark.low_color), high: this.#percentHelper.calcWatermark(this._highValue.value), high_color: ThemeManager.adaptColor(watermark.high_color), } : null; } get hasEntityCollection() { return this.#entityCollection.count >= 2; } get entityCollectionPercentage() { return this.#entityCollection.getPercentages(); } // ─── PUBLIC API METHODS ─────────────────────────────────────────────────── refresh(hass) { super.refresh(hass); // _hassProvider, _currentValue, _lowValue, _highValue this.#maxValue.refresh(); this._configHelper.checkConfig(); this.#entityCollection.refreshAll(); if (!this.isAvailable) return; this.#updatePercentHelper(); this.#theme.value = this.#percentHelper.valueForThemes(this.#theme.isCustomTheme, this.#theme.isBasedOnPercentage); } // ─── PRIVATE METHODS ────────────────────────────────────────────────────── #updatePercentHelper() { // update this.#percentHelper.isTimer = this._currentValue.entityType.isTimer || this._currentValue.entityType.isDuration; const currentUnit = this.#getCurrentUnit(); this.#percentHelper.unit = currentUnit; this.#percentHelper.decimal = this.#getCurrentDecimal(currentUnit); if (this._currentValue.entityType.isTimer) { this.#setTimerValues(); } else if (this._currentValue.entityType.isCounter || this._currentValue.entityType.isNumber) { this.#setCounterValues(); } else { this.#setStdValues(); } this.#percentHelper.refresh(); } #setTimerValues() { Object.assign(this.#percentHelper, { isReversed: this.timerIsReversed, current: this._currentValue.value.current, min: this._currentValue.value.min, max: this._currentValue.value.max, }); } #setCounterValues() { Object.assign(this.#percentHelper, { current: this._currentValue.value.current, min: this._currentValue.value.min, max: this.#maxValue.isEntity ? (this.#maxValue.value?.current ?? this.#maxValue.value) : this._currentValue.value.max, }); } #setStdValues() { const currentValue = this.hasEntityCollection ? this.#entityCollection.getTotalValue() : this._currentValue.value; Object.assign(this.#percentHelper, { current: currentValue, min: this._configHelper.config.min_value, max: this.#maxValue.value?.current ?? this.#maxValue.value, }); } #getCurrentUnit() { if (this._configHelper.config.unit) return this._configHelper.config.unit; if (this.#maxValue.isEntity) return CARD.config.unit.default; const unit = this._currentValue.unit; return unit === null ? CARD.config.unit.default : unit; } #getCurrentDecimal(currentUnit) { if (is.integer(this._configHelper.config.decimal)) return this._configHelper.config.decimal; if (this._currentValue.precision) return this._currentValue.precision; if (this._currentValue.entityType.isTimer) return CARD.config.decimal.timer; if (this._currentValue.entityType.isCounter) return CARD.config.decimal.counter; if (this._currentValue.entityType.isDuration) return CARD.config.decimal.duration; if (['j', 'd', 'h', 'min', 's', 'ms', 'μs'].includes(this._currentValue.unit)) return CARD.config.decimal.duration; if (this._configHelper.config.unit) return this._configHelper.config.unit === CARD.config.unit.default ? CARD.config.decimal.percentage : CARD.config.decimal.other; return currentUnit === CARD.config.unit.default ? CARD.config.decimal.percentage : CARD.config.decimal.other; } } /****************************************************************************************** * 🛠️ CardView * ======================================================================================== * * A specialized card view implementation that extends ViewBase specifically for * rendering full card components. This class provides the complete card functionality * with proper configuration management through CardConfigHelper. * * @class CardView * @extends ViewBase * @description A concrete implementation of ViewBase designed for full card rendering. * This class uses CardConfigHelper to handle card-specific configuration * validation, processing, and management. It inherits all entity management, * theme handling, and state processing capabilities from ViewBase while * providing card-specific configuration logic. * * @see ViewBase For inherited functionality * @see CardConfigHelper For configuration management details */ class CardView extends ViewBase { _configHelper = new CardConfigHelper(); } class BadgeView extends ViewBase { _configHelper = new BadgeConfigHelper(); } class FeatureView extends ViewBase { _configHelper = new FeatureConfigHelper(); } class CardTemplateView extends ViewCore { _configHelper = new TemplateConfigHelper(); icon = null; } class BadgeTemplateView extends ViewCore { _configHelper = new BadgeTemplateConfigHelper(); icon = null; } /****************************************************************************************** * 🛠️ ResourceManager * ======================================================================================== * * ✅ Manage ressources: interval, timeout, listener, subscription. * * @class */ class ResourceManager { #debug = CARD_CONTEXT.debug.ressourceManager; #log = null; #resources = new Map(); #throttles = new Map(); constructor() { this.#log = initLogger(this, this.#debug, ['add', 'remove', 'cleanup']); } // ─── PUBLIC GETTERS / SETTERS ───────────────────────────────────────────── get list() { return [...this.#resources.keys()]; } get count() { return this.#resources.size; } // ─── PUBLIC API METHODS ─────────────────────────────────────────────────── add(cleanupFn, id) { if (!is.func(cleanupFn)) { throw new Error('Resource must be a function'); } const finalId = id || this.#generateUniqueId(); if (this.#resources.has(finalId)) { this.remove(finalId); this.#log.debug(`Remove: ${finalId}`); } this.#resources.set(finalId, cleanupFn); this.#log.debug(`Set: ${finalId}`); return finalId; } setInterval(handler, timeout, id) { this.#log.debug('Starting interval with id:', id); const timerId = setInterval(handler, timeout); this.#log.debug('Timer started with timerId:', timerId); this.add(() => { this.#log.debug('Stopping interval with id:', id); clearInterval(timerId); }, id); return id; } has(id) { return this.#resources.has(id); // Vérifie si un ID existe dans la Map } setTimeout(handler, timeout, id) { this.#log.debug('Starting timeout with id:', id); const timerId = setTimeout(handler, timeout); this.#log.debug('Timeout started with timerId:', timerId); return this.add(() => clearTimeout(timerId), id); } addEventListener(target, event, handler, options, id) { target.addEventListener(event, handler, options); return this.add(() => target.removeEventListener(event, handler, options), id); } addSubscription(unsubscribeFn, id) { return this.add(() => { unsubscribeFn(); }, id); } throttle(fn, delay, id) { if (!this.#throttles.has(id)) { this.#throttles.set(id, { lastCall: 0 }); this.add(() => this.resetThrottle(id), id); } const context = this.#throttles.get(id); const now = Date.now(); if (now - context.lastCall >= delay) { context.lastCall = now; fn(); this.#log.debug('Throttle function - ', id); } } throttleDebounce(fn, delay, id) { const now = Date.now(); const keys = { throttle: `${id}-throttle`, debounce: `${id}-debounce`, }; // Throttle — exec if time is over if (!this.#throttles.has(keys.throttle)) { this.#throttles.set(keys.throttle, { lastCall: 0 }); this.add(() => this.resetThrottle(keys.throttle), keys.throttle); } const context = this.#throttles.get(keys.throttle); if (now - context.lastCall >= delay) { context.lastCall = now; fn(); this.#log.debug('ThrottleDebounce immediate - ', id); } // Debounce — exec after delay if (this.#resources.has(keys.debounce)) { this.remove(keys.debounce); } this.setTimeout( () => { fn(); this.#log.debug('ThrottleDebounce trailing - ', id); }, delay, keys.debounce, ); } resetThrottle(id) { this.#throttles.delete(id); } remove(id) { const cleanupFn = this.#resources.get(id); if (cleanupFn) { try { cleanupFn(); } catch (e) { console.error(`[ResourceManager] Error while removing '${id}'`, e); } this.#resources.delete(id); this.#log.debug(`Removed: ${id}`); } } cleanup() { for (const [id, cleanupFn] of this.#resources) { try { cleanupFn(); } catch (e) { console.error(`[ResourceManager] Error while clearing '${id}'`, e); } this.#log.debug(`Cleared: ${id}`); } this.#resources.clear(); this.#throttles.clear(); this.#log.debug('All resources cleared.'); } // ─── PRIVATE METHODS ────────────────────────────────────────────────────── #generateUniqueId() { // eslint-disable-next-line no-useless-assignment let id = null; do { id = Math.random().toString(36).slice(2, 8); } while (this.#resources.has(id)); return id; } } /****************************************************************************************** * 🛠️ DOMHelper * ======================================================================================== * * ✅ Manages DOM elements, RAF queue, and applied values cache. * * // Init * this._dom = new DOMHelper(); * this._dom.register("card", this.shadowRoot.querySelector(".card")); * this._dom.register("title", this.shadowRoot.querySelector(".title")); * * // Mises à jour — dédupliquées + batchées automatiquement * this._dom.setStyle("card", "--color-bg", "#fff"); * this._dom.setText ("title", "Température"); * this._dom.setHTML ("card", "..."); * * // Destroy * this._dom.destroy(); * * @class */ class DOMHelper { #debug = CARD_CONTEXT.debug.ressourceManager; #log = null; constructor() { this.#log = initLogger(this, this.#debug, ['register', 'unregister', 'destroy']); this._domElements = new Map(); // key → HTMLElement this._appliedValues = new Map(); // "key:prop" → last applied value this._pendingUpdates = new Map(); // "key:prop" → pending update function this._rafScheduled = false; } // ─── Element registration ───────────────────────────────────────────────── /** * Registers a DOM element under a given key. */ register(key, element) { this.#log.debug('DOMHelper.register(key, element):', { key, element }); this._domElements.set(key, element); } /** * Returns the DOM element associated with the given key. */ get(key) { return this._domElements.get(key); } /** * Unregisters a DOM element and clears its associated cache entries. */ unregister(key) { this._domElements.delete(key); for (const cacheKey of this._appliedValues.keys()) { if (cacheKey.startsWith(`${key}:`)) { this._appliedValues.delete(cacheKey); } } } // ─── RAF queue ──────────────────────────────────────────────────────────── /** * Enqueues a DOM update identified by a unique key + prop combination. * If the same key:prop is enqueued multiple times, only the latest function runs. * Schedules a single RAF flush if not already pending. */ enqueue(key, prop, updateFn) { this._pendingUpdates.set(`${key}:${prop}`, updateFn); if (!this._rafScheduled) { this._rafScheduled = true; requestAnimationFrame(() => this._flush()); } } /** * Flushes all pending updates in a single RAF callback. */ _flush() { const updates = this._pendingUpdates; this._pendingUpdates = new Map(); this._rafScheduled = false; updates.forEach((fn) => fn()); } // ─── DOM helpers with cache ─────────────────────────────────────────────── /** * Sets a CSS custom property on the element registered under the given key. * Skipped if the value matches the cache — no DOM read required. */ setStyle(key, prop, value) { if (value == null) return; const cacheKey = `${key}:style:${prop}`; if (this._appliedValues.get(cacheKey) === value) return; const el = this._domElements.get(key); if (!el) return; this.enqueue(key, `style:${prop}`, () => { el.style.setProperty(prop, value); this._appliedValues.set(cacheKey, value); }); } /** * Sets a CSS custom property synchronously — no RAF, no cache check, no queue. * Use when immediate DOM update is required. */ setStyleNow(key, prop, value) { if (value == null) return; const el = this._domElements.get(key); if (!el) return; el.style.setProperty(prop, value); this._appliedValues.set(`${key}:style:${prop}`, value); // ← met à jour le cache après } /** * Sets the text content of the element registered under the given key. * Skipped if the value matches the cache. */ setText(key, value) { if (value == null) return; const cacheKey = `${key}:text`; if (this._appliedValues.get(cacheKey) === value) return; const el = this._domElements.get(key); if (!el) return; this.enqueue(key, 'text', () => { el.textContent = value; this._appliedValues.set(cacheKey, value); }); } /** * Sets the inner HTML of the element registered under the given key. * Skipped if the value matches the cache. */ setHTML(key, value) { if (value == null) return; const cacheKey = `${key}:html`; if (this._appliedValues.get(cacheKey) === value) return; const el = this._domElements.get(key); if (!el) return; this.enqueue(key, 'html', () => { el.innerHTML = value; this._appliedValues.set(cacheKey, value); }); } /** * Toggles a CSS class on the element registered under the given key. * Skipped if the state matches the cache. */ toggleClass(key, className, force) { if (!className) return; const cacheKey = `${key}:class:${className}`; if (this._appliedValues.get(cacheKey) === force) return; const el = this._domElements.get(key); if (!el) return; this.enqueue(key, `class:${className}`, () => { el.classList.toggle(className, force); this._appliedValues.set(cacheKey, force); }); } /** * Adds one or more CSS classes to the element registered under the given key. * Batched via the RAF queue. */ addClass(key, ...classes) { const el = this._domElements.get(key); if (!el) return; const filtered = classes.filter(Boolean); if (!filtered.length) return; this.enqueue(key, `addClass:${filtered.join(',')}`, () => { el.classList.add(...filtered); }); } /** * Sets an attribute on the element registered under the given key. * Skipped if the value matches the cache. */ setAttribute(key, attr, value) { if (value == null) return; const cacheKey = `${key}:attr:${attr}`; if (this._appliedValues.get(cacheKey) === value) return; const el = this._domElements.get(key); if (!el) return; this.enqueue(key, `attr:${attr}`, () => { el.setAttribute(attr, value); this._appliedValues.set(cacheKey, value); }); } // ─── Walkthrough ────────────────────────────────────────────────────────── static walkUpThroughShadow(node, selector) { if (!node) return null; if (node instanceof ShadowRoot) return DOMHelper.walkUpThroughShadow(node.host, selector); if (node instanceof HTMLElement && node.matches(selector)) return node; return DOMHelper.walkUpThroughShadow(node.parentNode, selector); } // ─── Cleanup ────────────────────────────────────────────────────────────── /** * Clears all internal state: elements, cache, and pending updates. * Should be called when the component is destroyed. */ destroy() { this._domElements.clear(); this._appliedValues.clear(); this._pendingUpdates.clear(); this._rafScheduled = false; } } /****************************************************************************************** * 🛠️ ActionHelper — Utility Class * ======================================================================================== * * ✅ Centralized handler for `xyz_action` logic. * Deprecated for HA 2026.3+ * * 📌 Purpose: * - Encapsulates and manages the execution, validation, and dispatch of `xyz_action`. * - Promotes reusable, maintainable logic for action-related features. */ class ActionHelper { #target = null; #config = null; #fromIcon = false; #iconClickSources = new Set(['shape', 'ha-svg-icon', 'img']); constructor(target) { this.#target = target; } init(config, disableIconTap) { this.#config = config; if (!this.#target) return; document.querySelector('action-handler').bind(this.#target, { hasHold: true, hasDoubleClick: true, }); this.#target.addEventListener( 'pointerdown', (ev) => { const localName = ev.composedPath()[0].localName; this.#fromIcon = !disableIconTap && this.#iconClickSources.has(localName); }, { passive: true }, ); this.#target.addEventListener('action', (ev) => { this.#handleAction(ev, this.#fromIcon); }); } #handleAction(ev, fromIcon) { const action = ev.detail.action; const iconActionKey = `icon_${action}_action`; const actionConfig = fromIcon && this.#config[iconActionKey]?.action !== 'none' ? this.#config[iconActionKey] : this.#config[`${action}_action`]; if (!actionConfig) return; this.#target.dispatchEvent( new CustomEvent('hass-action', { bubbles: true, composed: true, detail: { config: { entity: this.#config.entity, tap_action: actionConfig, }, action: 'tap', }, }), ); } } /****************************************************************************************** * 🛠️ HACore * ======================================================================================== * * Base class for Home Assistant custom elements (cards, badges, features). * * HTMLElement * │ * ├── HACore * │ ├── HABase * │ │ ├── EntityProgressCardBase * │ │ │ ├── EntityProgressCard * │ │ │ └── EntityProgressBadge * │ │ └── EntityProgressTemplateBase * │ │ ├── EntityProgressTemplateCard * │ │ └── EntityProgressTemplateBadge * │ │ * │ └── EntityProgressFeatures * * Provides: * - Shadow DOM initialization and lifecycle management (connectedCallback, disconnectedCallback) * - Configuration handling via setConfig() * - Hass state tracking and change detection * - DOM rendering pipeline: render() → _createCardElements() → _buildStyle() * - Batched DOM updates via DOMHelper (RAF queue + value cache) * - Jinja2 template subscriptions via WebSocket * - Resource lifecycle management (listeners, subscriptions, intervals) * * Subclasses MUST implement: * - _handleHassUpdate() → react to hass state changes * - _updateCSS() → apply dynamic CSS custom properties * - validJinjaFields → current valid jinja available / config * - _getJinjaHandlers() → handle Jinja2 template results * * @abstract * @extends HTMLElement */ class HACore extends HTMLElement { static version = VERSION; static _baseClass = META.types.feature.typeName; static _cardStructure = new FeatureStructure(); static _cardStyle = CARD_CSS; static _cardElement = CARD.htmlStructure.card.element; _debug = CARD_CONTEXT.debug.card; _log = null; _resourceManager = null; _cardView = new FeatureView(); _dom = new DOMHelper(); _hassProvider = HassProviderSingleton.getInstance(); _changeTracker = new ChangeTracker(); #isRendered = false; // ─── LIFECYCLE METHODS === static get _loggedMethods() { return [ 'connectedCallback', 'disconnectedCallback', 'setConfig', 'refresh', 'reset', 'render', '_storeDOM', '_manageStructureOptions', '_handleWatermarkClasses', '_handleBarEffect', '_updateDynamicElements', '_renderJinja', '_refreshBarEffect', '_createCardElements', '_buildStyle', '_processJinjaFields', '_subscribeToTemplate', '_watchWebSocket', '_unwatchWebSocket', '_validateProcessJinjaFields', // abstract '_handleHassUpdate', '_updateCSS', '_getJinjaHandlers', ]; } constructor() { super(); this._log = initLogger(this, this._debug, this.constructor._loggedMethods); this.attachShadow({ mode: CARD.config.shadowMode }); } static getConfigElement() { // // customize it // return null; } connectedCallback() { if (!this._resourceManager) this._resourceManager = new ResourceManager(); this.render(); this._updateDynamicElements(); if (this.hass) { this._handleHassUpdate(); this._watchWebSocket(); } } disconnectedCallback() { this._resourceManager?.cleanup(); this._resourceManager = null; } // ─── PUBLIC API METHODS ─────────────────────────────────────────────────── /** * Updates the component's configuration and triggers static changes. */ setConfig(config) { this._log.debug('📎 HACore.setConfig()', config); if (!config) throw new Error('setConfig: invalid config'); if (this.isRendered) this.reset(); // Card/Badge editor this._cardView.config = { ...config }; if (is.string(config.entity)) this._changeTracker.watchEntity(config.entity); if (is.string(config.max_value)) this._changeTracker.watchEntity(config.max_value); if (is.string(config?.watermark?.low)) this._changeTracker.watchEntity(config.watermark.low); if (is.string(config?.watermark?.high)) this._changeTracker.watchEntity(config.watermark.high); this.render(); // re-build the card if (this.hass) this._handleHassUpdate(); // Card/Badge editor } /** * Sets the Home Assistant (`hass`) instance and updates dynamic elements. * * @param {Object} hass - The Home Assistant instance containing the current * state and services. */ set hass(hass) { this._log.debug('👉 HACore.set hass()'); if (!hass) return; const isFirstHass = !this.hass; this._changeTracker.hassState = hass; if (isFirstHass || this._changeTracker.hassState.isUpdated) { this._hassProvider.hass = hass; this._handleHassUpdate(); } if (!this._resourceManager) this._resourceManager = new ResourceManager(); if (!this._wsInitialized) this._watchWebSocket(); } get hass() { return this._hassProvider.hass; } _handleHassUpdate() { throw new Error(`${this.constructor.name} must implement _handleHassUpdate()`); // must be overrided. // this.refresh(); // customization // ... } refresh() { this._cardView.refresh(this.hass); this._updateDynamicElements(); } get isRendered() { return this.#isRendered; } reset() { this.#isRendered = false; this._dom.destroy(); if (this.shadowRoot) { this.shadowRoot.innerHTML = ''; // purge le contenu shadow DOM } } get innerHTML() { return this.constructor._cardStructure.innerHTML; } get cardStyle() { return this.constructor._cardStyle; } get baseClass() { return this.constructor._baseClass; } get cardElement() { return this.constructor._cardElement; } // ─── CARD BUILDING ──────────────────────────────────────────────────────── /** * Builds and initializes the structure of the custom card component. * * This method creates the visual and structural elements of the card and injects * them into the component's Shadow DOM. */ render() { if (this.isRendered) return; this.#isRendered = true; this._manageStructureOptions(); const element = this._createCardElements(); this.shadowRoot.replaceChildren(element.style, element.card); this._storeDOM(); requestAnimationFrame(() => { this._dom.addClass(CARD.htmlStructure.card.element, 'transition-ready'); }); } _manageStructureOptions() { // // customize it // this.constructor._cardStructure.options = { barType: this._cardView.config.center_zero ? 'centerZero' : 'default', // ─── true barPosition: this._cardView.config.bar_position, }; } _createCardElements() { const style = document.createElement(CARD.style.element); style.textContent = this.cardStyle; const card = document.createElement(this.cardElement); this._dom.destroy(); this._dom.register(CARD.htmlStructure.card.element, card); this._dom.setStyle(CARD.htmlStructure.card.element, CARD.style.dynamic.progressBar.value.var, 0); if (this._cardView.hasClickableCard || this._cardView.hasClickableIcon) { Object.entries(CARD.htmlStructure.card.extraAttr).forEach(([key, value]) => { this._dom.setAttribute(CARD.htmlStructure.card.element, key, value); }); } this._buildStyle(); card.innerHTML = this.innerHTML; return { style, card }; } _storeDOM() { const allElements = this.shadowRoot.querySelectorAll('*'); allElements.forEach((el) => { if (el.classList.length > 0) { const key = el.classList[0]; // 1st class only this._dom.register(key, el); } }); } _buildStyle() { // // customize it // this._addBaseClasses(); this._handleWatermarkClasses(); this._handleBarEffect(); } _addBaseClasses() { this._dom.addClass( CARD.htmlStructure.card.element, this.baseClass, this._cardView.config.layout, this._cardView.config.bar_size, this._cardView.config.bar_orientation ? CARD.style.dynamic.progressBar.orientation[this._cardView.config.bar_orientation] : null, this._cardView.config.center_zero ? CARD.style.dynamic.progressBar.centerZero.class : null, (this._cardView.config.layout === 'vertical' && this._cardView.config.bar_orientation === 'up' && this._cardView.config.bar_position === 'overlay') || (this._cardView.config.bar_orientation === 'up' && this._cardView.config.bar_position === 'background') ? 'vertical-bar' : 'horizontal-bar', ); } _handleWatermarkClasses() { if (!this._cardView.hasWatermark) return; const type = ['area', 'blended', 'striped', 'line', 'triangle', 'round'].includes(this._cardView.watermark.type) ? `${this._cardView.watermark.type}` : 'blended'; const showClass = CARD.style.dynamic.show; this._dom.toggleClass(CARD.htmlStructure.card.element, `${showClass}-hwm`, !this._cardView.watermark.disable_high); this._dom.toggleClass(CARD.htmlStructure.card.element, `hwm-${type}`, !this._cardView.watermark.disable_high); this._dom.toggleClass(CARD.htmlStructure.card.element, `${showClass}-lwm`, !this._cardView.watermark.disable_low); this._dom.toggleClass(CARD.htmlStructure.card.element, `lwm-${type}`, !this._cardView.watermark.disable_low); } _handleBarEffect(jinjaEffect = null) { this._log.debug('📎 HACore _handleBarEffect(jinjaEffect)', jinjaEffect); if (!this._cardView.barEffectsEnabled) return; const isJinja = is.jinja(this._cardView.config.bar_effect); if (isJinja && !jinjaEffect) return; const effects = Object.values(CARD.style.dynamic.progressBar.effect); effects.forEach((effect) => { const active = isJinja ? jinjaEffect.includes(effect.label) : this._cardView.hasBarEffect(effect.label); this._dom.toggleClass(CARD.htmlStructure.card.element, effect.class, active); }); } // ─── DYNAMIC ELEMENTS UPDATE ────────────────────────────────────────────── _updateDynamicElements() { // // cutomize it // this._updateCSS(); this._processJinjaFields(); } // ─── CSS MANAGEMENT ─────────────────────────────────────────────────────── _updateCSS() { // // cutomize it - Dynamique CSS values // throw new Error(`${this.constructor.name} must implement _updateCSS()`); } _applyProgressCSS(progressValue, { barColor = null, iconColor = null, isCenterZero = false } = {}) { const cardKey = CARD.htmlStructure.card.element; if (barColor !== null) this._dom.setStyle(cardKey, CARD.style.dynamic.progressBar.color.var, barColor); if (iconColor !== null) this._dom.setStyle(cardKey, CARD.style.dynamic.iconAndShape.color.var, iconColor); if (progressValue !== null) { if (isCenterZero) { this._dom.toggleClass('inner', 'negative', progressValue < 0); this._dom.toggleClass('inner', 'positive', progressValue >= 0); } else { this._dom.addClass('inner', 'positive'); } this._dom.setStyle(cardKey, CARD.style.dynamic.progressBar.value.var, progressValue); this._dom.setAttribute(CARD.htmlStructure.elements.progressBar.container.class, 'aria-valuenow', Math.round(progressValue * 100)); } } _applyWatermarkCSS(watermark, isCenterZero = false) { if (!watermark) return; const cardKey = CARD.htmlStructure.card.element; HACore._getWatermarkProperties(watermark, isCenterZero).forEach(([variable, value]) => { if (value != null) this._dom.setStyle(cardKey, variable, value); }); } // ─── WATERMARK MANAGEMENT ───────────────────────────────────────────────── static _getWatermarkProperties(watermark, isCenterZero = false) { const highWatermark = isCenterZero ? 50 + watermark.high / 2 : watermark.high; const lowWatermark = isCenterZero ? 50 + watermark.low / 2 : watermark.low; return [ [CARD.style.dynamic.watermark.high.value.var, `${highWatermark}%`], [CARD.style.dynamic.watermark.high.color.var, watermark.high_color], [CARD.style.dynamic.watermark.low.value.var, `${lowWatermark}%`], [CARD.style.dynamic.watermark.low.color.var, watermark.low_color], [CARD.style.dynamic.watermark.opacity.var, watermark.opacity], [CARD.style.dynamic.watermark.lineSize.var, watermark.line_size], ]; } // ─── JINJA TEMPLATE RENDERING ───────────────────────────────────────────── get validJinjaFields() { const result = Object.fromEntries( Object.keys(this._getJinjaHandlers()) .map((key) => [key, this._cardView.config[key] || '']) .filter(([, value]) => is.nonEmptyString(value)), ); this._log.debug('validJinjaFields: ', { handler: this._getJinjaHandlers(), config:this._cardView.config, result }); return result; } _getJinjaHandlers(content) { // // cutomize it - list the fields/render func // throw new Error(`${this.constructor.name} must implement _getJinjaHandlers(${content})`); /* return { badge_icon: () => this._renderBadgeIcon(content), // HABase badge_color: () => this._renderBadgeColor(content), // HABase bar_effect: () => this._refreshBarEffect(content), // HACore // ... }; */ } _renderJinja(key, content) { this._log.debug('📎 HACore._renderJinja():', { key, content }); const renderHandlers = this._getJinjaHandlers(content); const handler = renderHandlers[key]; if (handler) { handler(); } else { console.error(`Jinja - Unknown case: ${key}`); } } _refreshBarEffect(content) { this._log.debug('📎 HACore._refreshBarEffect():', { content }); const jinjaEffect = content.split(',').map((s) => s.trim()); this._handleBarEffect(jinjaEffect); } // ─── TEMPLATE PROCESSING ────────────────────────────────────────────────── get _wsInitialized() { return this._resourceManager?.has(CARD.network.disconnected) && this._resourceManager?.has(CARD.network.ready); } _unwatchWebSocket() { if (!this._resourceManager) return; this._resourceManager.remove(CARD.network.disconnected); this._resourceManager.remove(CARD.network.ready); } _watchWebSocket() { if (!this._resourceManager) return; // ISSUE 87 this._unwatchWebSocket(); this._resourceManager.addEventListener( this.hass.connection, 'disconnected', () => { this._log.debug('🔌 WebSocket disconnected'); const templates = this.validJinjaFields; for (const key of Object.keys(templates)) { this._resourceManager.remove(`template-${key}`); } }, { passive: true }, CARD.network.disconnected, ); this._resourceManager.addEventListener( this.hass.connection, 'ready', () => { this._log.debug('🔁 WebSocket ready — reprocessing templates'); if (!this._resourceManager) this._resourceManager = new ResourceManager(); // net reconnect this._processJinjaFields(); }, { passive: true }, CARD.network.ready, ); } _validateProcessJinjaFields() { return (this._cardView.config?.entity && this._cardView.hasStandardEntityError) || !this._resourceManager ? false : true; } _processJinjaFields() { if (!this._validateProcessJinjaFields()) { this._log.debug('❌ Jinja processing skipped - validation failed'); return; } this._log.debug('✅ Processing Jinja fields'); this._resourceManager?.throttleDebounce( () => { const templates = this.validJinjaFields; for (const [key, template] of Object.entries(templates)) { if (is.nonEmptyString(template)) this._subscribeToTemplate(key, template); } }, 300, 'jinjaProcess', ); } _getTemplateContext() { const entity = this._cardView?.config?.entity; return entity ? { entity } : {}; } async _subscribeToTemplate(key, template) { this._log.debug('📎 HACore._subscribeToTemplate:', { key, template }); const subscriptionKey = `template-${key}`; if (!this.hass?.connection?.connected) { this._log.debug(`[Template ${key}] WebSocket not connected, skipping subscription.`); return; } this._log.debug('network ok...'); // Add null check right before using _resourceManager if (!this._resourceManager) { this._log.debug(`[Template ${key}] ResourceManager is null, skipping subscription.`); return; } try { this._log.debug('key:', key); this._log.debug('template:', template); const unsub = await this.hass.connection.subscribeMessage((msg) => this._renderJinja(key, msg.result), { type: 'render_template', template, //template: template, variables: this._getTemplateContext(), }); // Check again after the async operation if (!this._resourceManager) { this._log.debug(`[Template ${key}] ResourceManager became null during subscription, cleaning up.`); unsub(); // Clean up the subscription return; } else if (!this.isConnected) { // DOM conn X unsub(); // Clean up the subscription return; } else { this._resourceManager.remove(subscriptionKey); this._resourceManager.addSubscription(unsub, subscriptionKey); } } catch (error) { this._log.error(`Failed to subscribe to template ${key}:`, error); } } } /****************************************************************************************** * 🛠️ HABase * ======================================================================================== * * ✅ Represents the base class for all custom "entity-progress" cards: * * 📌 Purpose: * - Provides shared structure, lifecycle hooks, and utility logic for custom Lovelace cards. * - Serves as the foundation for building consistent and reusable UI components. * * 🛠️ Example: * class MyCustomCard extends HABase { ... } * * 📚 Context: * - Designed for use in Home Assistant dashboards. * - Enables unified behavior across multiple card implementations. * * @class * @extends HACore */ class HABase extends HACore { static _baseClass = META.types.card.typeName; static _cardStructure = new CardStructure(); static _hasDisabledIconTap = false; static _hasDisabledBadge = false; static _hiddenComponents = [ // customize it CARD.style.dynamic.hiddenComponent.icon, CARD.style.dynamic.hiddenComponent.name, CARD.style.dynamic.hiddenComponent.secondary_info, CARD.style.dynamic.hiddenComponent.progress_bar, ]; _trendIcons = { up: HA_CONTEXT.icons.chevronUpBox, down: HA_CONTEXT.icons.chevronDownBox, flat: HA_CONTEXT.equalBox, }; _icon = null; _cardView = new CardView(); _actionHelper = null; #jinjaStateBadge = { icon: false, color: false }; #lastMessage = null; // ─── LIFECYCLE METHODS === static get _loggedMethods() { return [ ...super._loggedMethods, '_showIcon', '_handleImgIcon', '_handleStateIcon', '_createStateObjIcon', '_cleanupImgIcon', '_showBadge', '_enableBadge', '_setBadgeIconColor', '_setBadgeIcon', '_setBadgeColor', '_manageShape', '_updateTrend', '_addBaseClasses', '_addBaseParameter', '_applyConditionalClasses', '_handleHiddenComponents', '_manageErrorMessage', '_renderMessage', '_renderBadgeIcon', '_renderBadgeColor', '_processStandardFields', '_startAutoRefresh', '_stopAutoRefresh', ]; } constructor() { super(); this._actionHelper = new ActionHelper(this); } connectedCallback() { super.connectedCallback(); // render, _updateDynamicElements, watchWebSocket this._actionHelper.init(this._cardView.config, this.hasDisabledIconTap); } // disconnectedCallback() {} getCardSize() { if (![META.types.card.typeName, META.types.template.typeName].includes(this.baseClass)) return undefined; const cardSize = this._cardView.cardSize; this._log.debug('getCardSize: ', cardSize); return cardSize; } getLayoutOptions() { if (![META.types.card.typeName, META.types.template.typeName].includes(this.baseClass)) return undefined; const cardLayoutOptions = this._cardView.cardLayoutOptions; this._log.debug('getLayoutOptions: ', cardLayoutOptions); return cardLayoutOptions; } // ─── PUBLIC API METHODS ─────────────────────────────────────────────────── refresh() { this._cardView.refresh(this.hass); if (this._manageErrorMessage()) return; this._updateDynamicElements(); } reset() { super.reset(); // #isRendered, _dom.destroy(), shadowRoot.innerHTML this._icon = null; } get hasDisabledIconTap() { // check it soon return this.constructor._hasDisabledIconTap; } // ─── AUTO-REFRESH MANAGEMENT ────────────────────────────────────────────── _startAutoRefresh() { if (!this._resourceManager) return; this._resourceManager.setInterval( () => { this.refresh(this.hass); if (!this._cardView.isActiveTimer) { this._stopAutoRefresh(); } }, this._cardView.refreshSpeed, 'autoRefresh', ); } _stopAutoRefresh() { if (this._resourceManager) this._resourceManager.remove('autoRefresh'); } // ─── ERROR MESSAGE MANAGEMENT ───────────────────────────────────────────── _manageErrorMessage() { if (this._cardView.msg && (is.nullish(this._cardView.entity) || (this._cardView.isAvailable && !this._cardView.hasValidatedConfig))) { this._renderMessage(this._cardView.msg); return true; } this.#lastMessage = null; return false; } /** * Displays an error alert with the provided message. * 'info', 'warning', 'error' */ _renderMessage(msg) { if (msg === this.#lastMessage) return; this.#lastMessage = msg; // ha-alert exists ? let alert = this.shadowRoot.querySelector('ha-alert'); if (!alert) { alert = document.createElement('ha-alert'); this.shadowRoot.replaceChildren(alert); } // update the message alert.setAttribute('alert-type', msg.sev); // IMPORTANT: attribut alert.textContent = msg.content; } // ─── CARD BUILDING === _manageStructureOptions() { // // customize it // this.constructor._cardStructure.options = { barType: this._cardView.config.center_zero ? 'centerZero' : 'default', // ─── true layout: this._cardView.config.layout, barPosition: this._cardView.config.bar_position, barSingleLine: this._cardView.config.bar_single_line, trendIndicator: this._cardView.config.trend_indicator, }; } _buildStyle() { // // customize it // super._buildStyle(); // _handleWatermarkClasses, _handleBarEffect this._addBaseClasses(); this._addBaseParameter(); this._applyConditionalClasses(); this._handleHiddenComponents(); } _addBaseClasses() { super._addBaseClasses(); this._dom.addClass( CARD.htmlStructure.card.element, this.baseClass.includes('badge') ? 'progress-badge' : null, this._cardView.config.bar_position, this._cardView.hasReversedSecondaryInfoRow ? 'row-reverse' : null, this._cardView.config.text_shadow ? 'text-shadow' : null, ); } _addBaseParameter() { const cardKey = CARD.htmlStructure.card.element; const config = this._cardView.config; [ [this._cardView.hasReversedSecondaryInfoRow, '--secondary-info-row-reverse', 'row-reverse'], [config.min_width, CARD.style.dynamic.card.minWidth.var, config.min_width], [config.height, CARD.style.dynamic.card.height.var, config.height], [config.bar_max_width, CARD.style.dynamic.progressBar.maxWidth.var, config.bar_max_width], ].forEach(([condition, prop, value]) => { if (condition) this._dom.setStyle(cardKey, prop, value); }); } get conditionalStyle() { return new Map([ [CARD.style.dynamic.clickable.card, this._cardView.hasClickableCard], [CARD.style.dynamic.clickable.icon, this._cardView.hasClickableIcon], [CARD.style.dynamic.frameless.class, this._cardView.config.frameless], [CARD.style.dynamic.marginless.class, this._cardView.config.marginless], ]); } _applyConditionalClasses() { this.conditionalStyle.forEach((condition, className) => { this._dom.toggleClass(CARD.htmlStructure.card.element, className, condition); }); } _handleHiddenComponents(jinjaContent = null) { if (jinjaContent === null && is.jinja(this._cardView.config.hide)) return; const items = jinjaContent ?.split(',') .map((s) => s.trim()) .filter(Boolean) ?? null; this.constructor._hiddenComponents.forEach((component) => { this._dom.toggleClass( CARD.htmlStructure.card.element, component.class, items ? items.includes(component.label) : this._cardView.hasComponentHiddenFlag(component.label), ); }); } // ─── DYNAMIC ELEMENTS UPDATE ────────────────────────────────────────────── _updateDynamicElements() { // // cutomize it // this._showIcon(); this._showBadge(); this._manageShape(); this._updateTrend(); this._updateCSS(); this._processJinjaFields(); this._processStandardFields(); } // ─── Update Trend === _updateTrend() { if (!this._cardView.config.trend_indicator) return; this._dom.setAttribute( CARD.htmlStructure.elements.trendIndicator.icon.class, CARD.style.icon.badge.default.attribute, this._trendIcons[this._cardView.getTrend()], ); } // ─── CSS MANAGEMENT ─────────────────────────────────────────────────────── _updateCSS() { // // cutomize it - Dynamique CSS values // throw new Error(`${this.constructor.name} must implement _updateCSS()`); } // ─── ICON MANAGEMENT ────────────────────────────────────────────────────── _createImgIcon(altText, className = 'custom-icon-img') { this._log.debug('📎 HABase._createImgIcon():', { altText, className }); const img = document.createElement('img'); img.className = className; img.decoding = 'async'; img.alt = altText; return img; } _handleImgIcon(stateObj, srcPicture) { this._log.debug('📎 HABase._handleImgIcon():', { stateObj, srcPicture }); const pictureAlt = stateObj?.attributes?.friendly_name || 'Entity picture'; const iconContainer = this._dom.get(CARD.htmlStructure.elements.icon.class); if (!iconContainer) return; if (this._icon?.tagName !== 'IMG') { this._icon?.remove(); this._icon = this._createImgIcon(pictureAlt); iconContainer.replaceChildren(this._icon); this._dom.setStyle(CARD.htmlStructure.card.element, CARD.style.dynamic.iconAndShape.icon.size.var, '36px'); } this._icon.src = srcPicture; this._icon.alt = pictureAlt; } _createStateObjIcon(stateObj, curIcon, hasIconOverride, hasPicture) { this._log.debug('📎 HABase._createStateObjIcon():', { stateObj, curIcon, hasIconOverride, hasPicture }); if (!stateObj) { return this.isConnected ? { entity_id: 'sensor.dummy', state: 'unknown', attributes: { icon: curIcon || HA_CONTEXT.helpCircleOutline, friendly_name: 'Unknown Entity', }, } : null; } if (!hasIconOverride && !hasPicture) { return stateObj; } if (hasIconOverride) { return { entity_id: 'sensor.for_picture', state: 'on', attributes: { icon: curIcon, }, }; } const attributes = { ...stateObj.attributes }; if (hasPicture && !hasIconOverride) { delete attributes.entity_picture; } return { ...stateObj, attributes, }; } _cleanupImgIcon() { if (this._icon?.tagName === 'IMG') { this._icon.remove(); this._icon = null; } } _handleStateIcon(iconContainer, stateObjIcon) { this._log.debug('📎 HABase._handleStateIcon():', { iconContainer, stateObjIcon }); this._cleanupImgIcon(); if (!this._icon) { this._icon = document.createElement('ha-state-icon'); iconContainer.replaceChildren(this._icon); } this._icon.hass = this.hass; this._icon.stateObj = stateObjIcon; } _showIcon() { if (!this._cardView) return; const { entity: entityId, icon: curIcon } = this._cardView; const stateObj = this._hassProvider.getEntityStateObj(entityId); const hasIconOverride = is.nonEmptyString(curIcon); const srcPicture = this._hassProvider.getEntityProp(entityId, 'entity_picture'); const hasPicture = is.nonEmptyString(srcPicture); const iconContainer = this._dom.get(CARD.htmlStructure.elements.icon.class); if (!iconContainer) { this._log.error('Icon container not found for _showIcon.'); return; } if (hasIconOverride) { const stateObjIcon = this._createStateObjIcon(stateObj, curIcon, hasIconOverride, hasPicture); if (stateObjIcon) { this._handleStateIcon(iconContainer, stateObjIcon); } return; } if (hasPicture) { this._handleImgIcon(stateObj, srcPicture); return; } const stateObjIcon = this._createStateObjIcon(stateObj, curIcon, hasIconOverride, hasPicture); if (!stateObjIcon) return; this._handleStateIcon(iconContainer, stateObjIcon); } // ─── SHAPE MANAGEMENT ───────────────────────────────────────────────────── _manageShape() { this._dom.toggleClass( CARD.htmlStructure.card.element, CARD.style.dynamic.hiddenComponent.shape.class, !this._cardView.hasVisibleShape || this.hasDisabledIconTap, ); } // ─── BADGE MANAGEMENT ───────────────────────────────────────────────────── /** * Displays a badge (default info) */ _showBadge() { if (this.constructor._hasDisabledBadge) return; const badgeInfo = this._cardView.badgeInfo; const isBadgeEnable = badgeInfo || this._cardView.config.badge_icon || this._cardView.config.badge_color; this._enableBadge(isBadgeEnable); if (badgeInfo) this._setBadgeIconColor(badgeInfo.icon, badgeInfo.color, badgeInfo.backgroundColor); } _enableBadge(isBadgeEnable) { this._log.debug('📎 HABase._enableBadge():', { isBadgeEnable }); this._dom.toggleClass( CARD.htmlStructure.card.element, `${CARD.style.dynamic.show}-${CARD.htmlStructure.elements.badge.container.class}`, isBadgeEnable, ); } _setBadgeIconColor(icon, color, backgroundColor) { this._log.debug('📎 HABase._setBadgeIconColor():', { icon, color, backgroundColor }); this._setBadgeIcon(icon); this._setBadgeColor(color, backgroundColor); } _setBadgeIcon(icon) { this._dom.setAttribute(CARD.htmlStructure.elements.badge.icon.class, CARD.style.icon.badge.default.attribute, icon); } _setBadgeColor(color, backgroundColor) { this._dom.setStyle(CARD.htmlStructure.card.element, CARD.style.dynamic.badge.backgroundColor.var, backgroundColor); this._dom.setStyle(CARD.htmlStructure.card.element, CARD.style.dynamic.badge.color.var, color); } // ─── JINJA TEMPLATE RENDERING ───────────────────────────────────────────── /* _getJinjaHandlers(content) { // // cutomize it - list the fields/render func // return { badge_icon: () => this._renderBadgeIcon(content), // base badge_color: () => this._renderBadgeColor(content), // base bar_effect: () => this._refreshBarEffect(content), // base // ... }; }*/ _getJinjaBadgeState() { const hasIcon = this.#jinjaStateBadge.icon; const hasColor = this.#jinjaStateBadge.color; if (hasIcon && hasColor) return 'both'; if (hasIcon) return 'icon'; if (hasColor) return 'color'; return 'off'; } _updateBadgeVisibility() { const state = this._getJinjaBadgeState(); const shouldShow = state !== 'off'; this._enableBadge(shouldShow); } _renderBadgeIcon(content) { this._log.debug('📎 HABase._renderBadgeIcon():', { content }); this.#jinjaStateBadge.icon = is.nonEmptyString(content) && content.includes(HA_CONTEXT.icons.prefix); if (!is.nullish(this._cardView.badgeInfo)) return false; // alert -> cancel custom badge if (this.#jinjaStateBadge.icon) { this._setBadgeIcon(content); } this._updateBadgeVisibility(); } _renderBadgeColor(content) { this._log.debug('📎 HABase._renderBadgeColor():', { content }); this.#jinjaStateBadge.color = is.nonEmptyString(content); if (!is.nullish(this._cardView.badgeInfo)) return false; // alert -> cancel custom badge if (this.#jinjaStateBadge.color) { const backgroundColor = ThemeManager.adaptColor(content); const color = 'var(--white-color)'; this._setBadgeColor(color, backgroundColor); } this._updateBadgeVisibility(); } // ─── STD FIELDS PROCESSING ──────────────────────────────────────────────── static _getStandardFields(/*cardView*/) { // // customize it !!! // return []; } _processStandardFields() { this.constructor._getStandardFields(this._cardView).forEach(({ className, value }) => { this._dom.setText(className, value); }); } // ─── getStubConfig -> select entity ─────────────────────────────────────── static getStubEntity(hass) { return Object.keys(hass.states).find((id) => /^(sensor\..*battery|fan\.|cover\.|light\.)/i.test(id)) || 'sensor.temperature'; } } /****************************************************************************************** * 🛠️ EntityProgressCardBase * ======================================================================================== * * ✅ Represents the base class for all standard cards: * - EntityProgressCardBase / "entity-progress-card" * - EntityProgressBadge / "entity-progress-badge" * * * @class * @extends HABase */ class EntityProgressCardBase extends HABase { static _hiddenComponents = [ ...super._hiddenComponents, CARD.style.dynamic.hiddenComponent.value, ]; static get _loggedMethods() { return [...super._loggedMethods, '_getStandardFields', '_renderCustomInfo', '_renderNameInfo']; } _handleHassUpdate() { this.refresh(); if (!this._cardView.isActiveTimer) { this._stopAutoRefresh(); } else if (!this._resourceManager.has('autoRefresh')) { this._startAutoRefresh(); } } // ─── CSS - CUSTOMIZATION ────────────────────────────────────────────────── get conditionalStyle() { return new Map([...super.conditionalStyle, [CARD.style.dynamic.secondaryInfoError.class, this._cardView.hasStandardEntityError]]); } _updateCSS() { const bar = this._cardView; const progressValue = bar.percent / 100; this._applyProgressCSS(progressValue, { barColor: bar.barColor, iconColor: bar.iconColor, isCenterZero: bar.config.center_zero, }); this._applyWatermarkCSS(bar.hasWatermark ? bar.watermark : null, bar.config.center_zero); } // ─── STD FIELDS PROCESSING - CUSTOMIZATION ──────────────────────────────── static _getStandardFields(cardView) { return [ { className: CARD.htmlStructure.elements.nameMain.class, value: cardView.name, }, { className: CARD.htmlStructure.elements.secondaryInfoMain.class, value: cardView.secondaryInfoMain, }, ]; } // ─── JINJA TEMPLATE RENDERING - CUSTOMIZATION ───────────────────────────── _getJinjaHandlers(content) { return { badge_icon: () => this._renderBadgeIcon(content), // base badge_color: () => this._renderBadgeColor(content), // base bar_effect: () => this._refreshBarEffect(content), // base hide: () => this._handleHiddenComponents(content), // base custom_info: () => this._renderCustomInfo(content), name_info: () => this._renderNameInfo(content), }; } _renderCustomInfo(content) { this._dom.setHTML(CARD.htmlStructure.elements.secondaryInfoExtra.class, `${content} `); } _renderNameInfo(content) { this._dom.setHTML(CARD.htmlStructure.elements.nameExtra.class, ` ${content}`); } } /****************************************************************************************** * 📦 EntityProgressCard * ======================================================================================== * * ✅ HA CARD "entity-progress-card" * * @class * @extends EntityProgressCardBase */ class EntityProgressCard extends EntityProgressCardBase { _cardView = new CardView(); static _baseClass = META.types.card.typeName; // ─── STATIC METHODS === static get _loggedMethods() { return [...super._loggedMethods, 'getCardSize', 'getLayoutOptions']; } static getConfigElement() { return document.createElement(CARD_CONTEXT.dev ? `${META.types.card.editor}-dev` : META.types.card.editor); } static getStubConfig(hass) { return { type: `custom:${META.types.card.typeName}${CARD_CONTEXT.dev ? '-dev' : ''}`, entity: HABase.getStubEntity(hass), }; } } /****************************************************************************************** * 📦 EntityProgressBadge * ======================================================================================== * * ✅ HA CARD "entity-progress-badge" * * @class * @extends EntityProgressCardBase */ class EntityProgressBadge extends EntityProgressCardBase { _cardView = new BadgeView(); static _baseClass = META.types.badge.typeName; static _hasDisabledIconTap = true; static _hasDisabledBadge = true; static _cardStructure = new BadgeStructure(); static getConfigElement() { return document.createElement(CARD_CONTEXT.dev ? `${META.types.badge.editor}-dev` : META.types.badge.editor); } static getStubConfig(hass) { return { type: `custom:${META.types.badge.typeName}${CARD_CONTEXT.dev ? '-dev' : ''}`, entity: HABase.getStubEntity(hass), }; } // ─── JINJA TEMPLATE RENDERING - CUSTOMIZATION === _getJinjaHandlers(content) { return { bar_effect: () => this._refreshBarEffect(content), // base hide: () => this._handleHiddenComponents(content), // base custom_info: () => this._renderCustomInfo(content), name_info: () => this._renderNameInfo(content), }; } } /****************************************************************************************** * 📦 EntityProgressFeatures * ======================================================================================== * * ✅ HA CARD "entity-progress-feature" * * @class * @extends HACore */ class EntityProgressFeatures extends HACore { static _baseClass = META.types.feature.typeName; static _cardElement = 'div'; #firstHack = true; // ─── STATIC === static getStubConfig() { return { type: `custom:${META.types.feature.typeName}${CARD_CONTEXT.dev ? '-dev' : ''}`, }; } /** * Fixes the parent card layout when the feature is used as an overlay. * * By default, HA increases the card's --row-size by 1 for each feature added, * which would make the card taller. This method counteracts that behavior by * piercing through multiple Shadow DOM boundaries to directly manipulate the * parent card's layout properties. * * The following adjustments are made: * - `.container` and `hui-card-features` are set to `position: static` so the * feature can be positioned absolutely relative to `ha-card` * - `ha-card` gets `overflow: hidden` to clip the feature to the card's border radius * - `--row-size` is decremented by 1 to cancel the extra row reserved by HA * * A MutationObserver watches for HA re-applying `--row-size` and immediately * corrects it. A `fixing` flag prevents infinite loops between our correction * and the observer callback. * * Only executed once per instance via the `#firstHack` guard. * * @inspired by hass-progress-bar-feature (MIT License) — Copyright (c) ytilis * @see https://github.com/ytilis/hass-progress-bar-feature */ #fixCardStyles() { if (!['top', 'bottom'].includes(this._cardView.config.bar_position) || !this.#firstHack) return; const cardContainer = DOMHelper.walkUpThroughShadow(this, '.card'); if (!cardContainer) return; this.#firstHack = false; this._dom.register('ext:card', DOMHelper.walkUpThroughShadow(this, 'ha-card')); this._dom.register('ext:container', DOMHelper.walkUpThroughShadow(this, '.container')); this._dom.register('ext:features', DOMHelper.walkUpThroughShadow(this, 'hui-card-features')); this._dom.register('ext:card-container', cardContainer); const targetRowSize = parseInt(getComputedStyle(cardContainer)?.getPropertyValue(HA_CONTEXT.styles.rowSize)) - 1; let fixing = false; const fix = () => { if (fixing) return; const rowSize = getComputedStyle(cardContainer)?.getPropertyValue(HA_CONTEXT.styles.rowSize); if (rowSize && parseInt(rowSize) > targetRowSize) { fixing = true; this._dom.setStyleNow('ext:card', 'overflow', 'hidden'); this._dom.setStyleNow('ext:container', 'position', 'static'); this._dom.setStyleNow('ext:features', 'position', 'static'); this._dom.setStyleNow('ext:card-container', HA_CONTEXT.styles.rowSize, targetRowSize); fixing = false; } }; fix(); new MutationObserver(fix).observe(cardContainer, { attributes: true, attributeFilter: ['style'], }); } // ─── HANDLE UPDATE ──────────────────────────────────────────────────────── _handleHassUpdate() { this.#fixCardStyles(); this.refresh(); } // ─── CSS MANAGEMENT ─────────────────────────────────────────────────────── _updateCSS() { const bar = this._cardView; const progressValue = bar.percent / 100; this._applyProgressCSS(progressValue, { barColor: bar.barColor, isCenterZero: bar.config.center_zero, }); this._applyWatermarkCSS(bar.hasWatermark ? bar.watermark : null, bar.config.center_zero); } // ─── JINJA TEMPLATE RENDERING - CUSTOMIZATION ───────────────────────────── _getJinjaHandlers(content) { return { bar_effect: () => this._refreshBarEffect(content), // base }; } } /****************************************************************************************** * 🛠️ EntityProgressTemplateBase * ======================================================================================== * * ✅ HA CARD "entity-progress-card-template" * * @class * @extends HABase */ class EntityProgressTemplateBase extends HABase { static _cardStructure = new TemplateStructure(); // customize it _cardView = new CardTemplateView(); // customize it static get _loggedMethods() { return [ ...super._loggedMethods, '_updateWatermark', '_showIcon', '_forceJinjaProcessing', '_renderName', '_renderSecondary', '_managePercent', '_updateTrend', '_renderPercentCSS', '_validateProcessJinjaFields', ]; } connectedCallback() { super.connectedCallback(); // render, _updateDynamicElements, hass, watchWebSocket this._updateWatermark(); } _handleHassUpdate() { this.refresh(); // refresh() → _cardView.refresh() → _showIcon() → _updateCSS() // this._processJinjaFields(); // + Jinja } static getStubConfig(hass) { return { type: `custom:${META.types.template.typeName}${CARD_CONTEXT.dev ? '-dev' : ''}`, entity: HABase.getStubEntity(hass), ...CARD.config.stub.template, }; } // ─── CSS MANAGEMENT ─────────────────────────────────────────────────────── _updateCSS() { const bar = this._cardView; this._applyProgressCSS(null, { barColor: bar.barColor, iconColor: bar.iconColor, }); this._applyWatermarkCSS(bar.hasWatermark ? bar.watermark : null, bar.config.center_zero); } // ─── WATERMARK MANAGEMENT ───────────────────────────────────────────────── _updateWatermark() { if (!this._cardView.hasWatermark) return; this._cardView.refresh(); this._applyWatermarkCSS(this._cardView.watermark, this._cardView.config.center_zero); } // ─── ICON MANAGEMENT ────────────────────────────────────────────────────── _showIcon(iconFromJinja = null) { const jinjaIconNotReady = this._cardView.config.icon !== undefined && iconFromJinja === null; if (jinjaIconNotReady) return; this._cardView.icon = iconFromJinja; super._showIcon(); } // ─── JINJA TEMPLATE RENDERING - CUSTOMIZATION ───────────────────────────── _forceJinjaProcessing() { if (!this._resourceManager) this._resourceManager = new ResourceManager(); this._processJinjaFields(); } _getJinjaHandlers(content) { return { badge_icon: () => this._renderBadgeIcon(content), // base badge_color: () => this._renderBadgeColor(content), // base bar_effect: () => this._refreshBarEffect(content), // base hide: () => this._handleHiddenComponents(content), // base name: () => this._renderName(content), secondary: () => this._renderSecondary(content), icon: () => this._showIcon(content), percent: () => this._managePercent(content), color: () => this._dom.setStyle(CARD.htmlStructure.card.element, CARD.style.dynamic.iconAndShape.color.var, ThemeManager.adaptColor(content)), bar_color: () => this._dom.setStyle(CARD.htmlStructure.card.element, CARD.style.dynamic.progressBar.color.var, ThemeManager.adaptColor(content)), }; } _renderName(content) { this._dom.setHTML(CARD.htmlStructure.elements.nameMain.class, `${content}`.trim()); } _renderSecondary(content) { const hasLineBreak = //i.test(content); const wrappedContent = hasLineBreak ? `${content}` : `${content}`; this._dom.toggleClass(CARD.htmlStructure.card.element, 'info-multiline', hasLineBreak); this._dom.setHTML(CARD.htmlStructure.elements.secondaryInfoExtra.class, wrappedContent.trim()); } _managePercent(percent) { this._updateTrend(percent); this._renderPercentCSS(percent); } _updateTrend(percent) { if (!this._cardView.config.trend_indicator) return; this._dom.setAttribute( CARD.htmlStructure.elements.trendIndicator.icon.class, CARD.style.icon.badge.default.attribute, this._trendIcons[this._cardView.getTrend(percent)], ); } _renderPercentCSS(percent) { this._applyProgressCSS(percent / 100, { isCenterZero: this._cardView.config.center_zero, }); } // ─── TEMPLATE PROCESSING === _validateProcessJinjaFields() { return Boolean(this.hass) && Boolean(this._resourceManager); } } /****************************************************************************************** * 📦 EntityProgressTemplateCard * ======================================================================================== * * ✅ HA CARD "entity-progress-card-template" * * @class * @extends EntityProgressTemplateBase */ class EntityProgressTemplateCard extends EntityProgressTemplateBase { static _baseClass = META.types.template.typeName; static get _loggedMethods() { return [...super._loggedMethods, 'getCardSize', 'getLayoutOptions']; } static getConfigElement() { return document.createElement(`${META.types.template.editor}${CARD_CONTEXT.dev ? '-dev' : ''}`); } } /****************************************************************************************** * 📦 EntityProgressTemplateBadge * ======================================================================================== * * ✅ HA CARD "entity-progress-badge-template" * * @class * @extends EntityProgressTemplateBase */ class EntityProgressTemplateBadge extends EntityProgressTemplateBase { static _baseClass = META.types.badgeTemplate.typeName; static _hasDisabledIconTap = true; static _hasDisabledBadge = true; static _cardStructure = new BadgeStructure(); _cardView = new BadgeTemplateView(); static getConfigElement() { return document.createElement(`${META.types.badgeTemplate.editor}${CARD_CONTEXT.dev ? '-dev' : ''}`); } setConfig(config) { super.setConfig(config); if (this.hass) setTimeout(() => this.refresh(), 0); } static getStubConfig(hass) { return { type: `custom:${META.types.badgeTemplate.typeName}${CARD_CONTEXT.dev ? '-dev' : ''}`, entity: HABase.getStubEntity(hass), }; } } /****************************************************************************************** * 📦 CARD/BADGE EDITOR ******************************************************************************************/ const availableSpace = (gap = 16, factor = 0.5) => `calc((100% - ${gap}px) * ${factor})`; /****************************************************************************************** * 🛠️ EditorDOMHelper * ======================================================================================== * * @class * @extends DOMHelper */ class EditorDOMHelper extends DOMHelper { // ─── Field registration ─────────────────────────────────────────────────── /** * Registers a field element and its definition. * Wraps DOMHelper.register() — the def is stored on the element directly * so it travels with it without needing a separate Map. * * @param {string} name — field id * @param {HTMLElement} el — ha-selector or ha-form element * @param {object} def — field definition from _fields */ registerField(name, el, def) { el._fieldDef = def; this.register(name, el); } // ─── Hass propagation ───────────────────────────────────────────────────── /** * Propagates hass to all registered field elements. * Batched in a single RAF call. * * @param {object} hass */ updateHass(hass) { this.enqueue('__hass__', 'hass', () => { for (const el of this._domElements.values()) { if (el.hass !== hass) el.hass = hass; } }); } // ─── Field value ────────────────────────────────────────────────────────── /** * Updates the value of a ha-selector field. * Skipped if value hasn't changed. * * @param {string} name * @param {*} newVal */ updateValue(name, newVal) { this.enqueue(name, 'value', () => { const el = this._domElements.get(name); if (!el) return; if (el.value !== newVal) el.value = newVal; }); } // ─── Visibility ─────────────────────────────────────────────────────────── /** * Shows or hides a field. * Batched via RAF. * * @param {string} name * @param {boolean} visible */ updateVisibility(name, visible) { this.enqueue(name, 'display', () => { const el = this._domElements.get(name); if (!el) return; el.style.display = visible ? '' : 'none'; }); } // ─── Dynamic selector ───────────────────────────────────────────────────── /** * Updates the selector of a ha-selector field. * Used for fields whose options depend on another field (e.g. attribute → entity). * * @param {string} name * @param {object} selector */ updateSelector(name, selector) { this.enqueue(name, 'selector', () => { const el = this._domElements.get(name); if (!el) return; el.selector = selector; }); } // ─── Bulk update ────────────────────────────────────────────────────────── /** * Iterates all registered fields and applies value, visibility, * and dynamic selector updates based on the current config. * * @param {object} config — current card config * @param {function} resolveValue — (def, config) => raw value */ updateAll(config, resolveValue) { for (const [name, el] of this._domElements) { const def = el._fieldDef; if (!def) continue; // Visibility if (def.showIf) { this.updateVisibility(name, def.showIf(config)); } // Dynamic selector if (def.selectorOf) { this.updateSelector(name, { attribute: { entity_id: config[def.selectorOf] ?? '' } }); } // Context if (def.context) { this.enqueue(name, 'context', () => { const el = this._domElements.get(name); if (!el) return; el.context = Object.fromEntries( Object.entries(def.context).map(([k, v]) => [k, config[v] ?? '']) ); }); } // Champs virtuels — pas de valeur dans le config, géré par showIf uniquement if (def.virtual) { if (def.resolveVirtual) { const val = def.resolveVirtual(config); this.updateValue(name, val); } continue; } // Value const raw = resolveValue(def, config); const val = def.invert ? !raw : raw; this.updateValue(name, val); } } } /****************************************************************************************** * 🛠️ EditorBase * ======================================================================================== * * @class * @extends HTMLElement */ class EditorBase extends HTMLElement { static _fields = { /* --- cutomizee it general: { flat: true, fields: { entity: { name: 'entity', type: 'entity' }, }, }, content: { title: 'editor.title.content', icon: 'mdi:text-short', fields: { name: { name: 'name', type: 'template' }, secondary: { name: 'secondary', type: 'template' }, percent: { name: 'percent', type: 'template' }, }, }, */ }; // ─── private state ──────────────────────────────────────────────────────── #config = {}; #hassProvider = null; #dom = null; #boundOnChanged = null; _configHelper = new BaseConfigHelper(); // ─── LIFECYCLE ──────────────────────────────────────────────────────────── constructor() { super(); this.attachShadow({ mode: 'open' }); this.#hassProvider = HassProviderSingleton.getInstance(); this.#dom = new EditorDOMHelper(); } connectedCallback() { this.#boundOnChanged = this.#onChanged.bind(this); this.#render(); this.shadowRoot.addEventListener('value-changed', this.#boundOnChanged); } disconnectedCallback() { this.shadowRoot.removeEventListener('value-changed', this.#boundOnChanged); this.#boundOnChanged = null; // this.#dom.destroy(); } // ─── PUBLIC API ─────────────────────────────────────────────────────────── set hass(hass) { if (!hass) return; this.#hassProvider.hass = hass; this.#dom.updateHass(hass); } get hass() { return this.#hassProvider.hass; } setConfig(config) { if (!config) throw new Error(CARD.config.configError); // config with default value this._configHelper.config = config; // current config this.#config = config; this.#updateFields(); } // ─── RENDER (once) ──────────────────────────────────────────────────────── #render() { if (this.shadowRoot.querySelector('.editor')) return; const style = document.createElement('style'); style.textContent = ` .editor { display: flex; flex-direction: column; gap: 16px; } .panel-body { display: flex; flex-direction: row; gap: 16px; flex-wrap: wrap; align-content: flex-start; padding: 8px 0; } `; const container = document.createElement('div'); container.className = 'editor'; for (const [section, def] of Object.entries(this.constructor._fields)) { container.appendChild(this.#buildExpansionPanel(section, def)); } this.shadowRoot.append(style, container); } #buildExpansionPanel(section, def) { if (def.flat) { const frag = document.createDocumentFragment(); for (const field of Object.values(def.fields)) { frag.appendChild(this.#buildField(field)); } return frag; } const panel = document.createElement('ha-expansion-panel'); panel.header = this.#hassProvider.localize(def.title); panel.outlined = true; if (def.icon) { const icon = document.createElement('ha-icon'); icon.setAttribute('icon', def.icon); icon.slot = 'leading-icon'; panel.appendChild(icon); } const body = document.createElement('div'); body.className = 'panel-body'; for (const field of Object.values(def.fields)) { body.appendChild(this.#buildField(field)); } panel.appendChild(body); return panel; } #getSelectorForType(type) { const buildSelect = (opts) => ({ select: { options: Object.entries(opts).map(([value, label]) => ({ value, label })), mode: 'dropdown' }, }); const buildBoxSelect = (opts, imageFn = null) => ({ // see https://github.com/home-assistant/frontend/blob/dev/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts#L158 select: { mode: 'box', options: Object.entries(opts).map(([value, label]) => ({ value, label, ...(imageFn ? { image: imageFn(value) } : {}), })), }, }); const options = this.#hassProvider.localize('editor.option'); const tileImage = (value) => ({ // see https://github.com/home-assistant/frontend/blob/dev/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts#L158 src: `/static/images/form/tile_content_layout_${value}.svg`, src_dark: `/static/images/form/tile_content_layout_${value}_dark.svg`, flip_rtl: true, }); const selectors = { text: () => ({ text: { mode: 'box' } }), entity: () => ({ entity: {} }), entity_name: () => ({ entity_name: {} }), state_content: () => ({ ui_state_content: { allow_context: true } }), attribute: () => ({ attribute: { entity_id: this.#config.entity ?? '' } }), maxValueAttribute: () => ({ attribute: { entity_id: this.#config.max_value ?? '' } }), number: () => ({ number: {} }), decimal: () => ({ number: { min: 0, max: 10, mode: 'box' } }), slider: () => ({ number: { min: 0, max: 300, step: 1, mode: 'slider', unit_of_measurement: 'px' } }), template: () => ({ template: {} }), toggle: () => ({ boolean: {} }), action: () => ({ 'ui-action': {} }), icon: () => ({ icon: { icon_set: ['mdi'] } }), color: () => ({ 'ui-color': {} }), default: () => ({ text: { mode: 'box' } }), bar_size: () => buildSelect(options.bar_size), bar_orientation: () => buildSelect(options.bar_orientation), bar_position: () => buildSelect(options.bar_position), theme: () => buildSelect(options.theme), layout: () => buildBoxSelect(options.layout, tileImage), }; return (selectors[type] ?? (() => ({ text: {} })))(); } #resolveFieldMeta(field) { const isNested = field.name.includes('.'); const [, childKey] = isNested ? field.name.split('.') : [null, null]; const raw = EditorBase.#resolveValue(field, this._configHelper.config); const isInverted = field.invert ?? false; return { label: this.#hassProvider.localize('editor.field')[isNested ? `toggle_${childKey}` : field.name], value: isInverted ? !raw : raw, isInverted, }; } #buildField(field) { const el = document.createElement('ha-selector'); el.id = field.name; el.hass = this.hass; el.required = field.required ?? false; el.style.width = field.width ?? '100%'; el.isArray = field.array ?? false; el.selector = this.#getSelectorForType(field.type); if (field.isInGroup) el.classList.add(field.isInGroup); const { label, value, isInverted } = this.#resolveFieldMeta(field); el.label = label; el.value = value; el.isInverted = isInverted; if (field.context) { el.context = Object.fromEntries( Object.entries(field.context).map(([k, v]) => [k, this.#config[v] ?? '']) ); } this.#dom.registerField(field.name, el, field); return el; } static #resolveValue(def, config) { const empty = ['toggle', 'number', 'decimal'].includes(def.type) ? undefined : ''; if (!config) return empty; if (def.virtual && def.resolveVirtual) return def.resolveVirtual(config); const isNested = def.name.includes('.'); const [parentKey, childKey] = isNested ? def.name.split('.') : [def.name, null]; const key = def.target ?? def.name; if (isNested && def.array) return config[parentKey]?.includes(childKey) ?? false; if (isNested) return config[parentKey]?.[childKey] ?? empty; return config[key] ?? empty; } // ─── UPDATE (every setConfig) ───────────────────────────────────────────── #updateFields() { this.#dom.updateAll(this._configHelper.config, EditorBase.#resolveValue); } // ─── EVENTS ─────────────────────────────────────────────────────────────── #handleVirtualField(def, value) { if (!def.onVirtualChange) return; const newConfig = def.onVirtualChange(value, { ...this.#config }); if (newConfig) this.#sendConfig(newConfig); } #handleNestedArrayField(parentKey, childKey, value) { const current = [...(this.#config[parentKey] ?? [])]; const updated = value ? [...current, childKey] : current.filter((v) => v !== childKey); this.#sendConfig({ ...this.#config, [parentKey]: updated }); } #handleNestedField(parentKey, childKey, value) { this.#sendConfig({ ...this.#config, [parentKey]: { ...this.#config[parentKey], [childKey]: value }, }); } #handleStdField(def, key, value) { const targetKey = def?.target ?? key; if (!value && def?.onClear) { this.#sendConfig(def.onClear({ ...this.#config })); return; } this.#sendConfig({ ...this.#config, [targetKey]: value }); } #onChanged(e) { const key = e.target.id; if (!key || !e.detail || !('value' in e.detail)) return; const def = this.#dom.get(key)?._fieldDef; let value = e.detail.value; if (def?.virtual) { this.#handleVirtualField(def, value); return; } const isInverted = e.target?.isInverted ?? false; const isArray = e.target.isArray ?? false; const isNested = key.includes('.'); const [parentKey, childKey] = isNested ? key.split('.') : []; if (isInverted) value = !value; if (isNested && isArray) this.#handleNestedArrayField(parentKey, childKey, value); else if (isNested) this.#handleNestedField(parentKey, childKey, value); else this.#handleStdField(def, key, value); } #sendConfig(config) { this.dispatchEvent( new CustomEvent('config-changed', { detail: { config }, bubbles: true, composed: true, }), ); } } /****************************************************************************************** * 🛠️ EditorFieldsType/EditorFactory * ======================================================================================== */ const field = (type) => (name, o = {}) => ({ name, type, ...o }); const EditorFieldsType = { entity: field('entity'), entityName: field('entity_name'), stateContent: field('state_content'), text: field('text'), number: field('number'), slider: field('slider'), decimal: field('decimal'), toggle: field('toggle'), tpl: field('template'), action: field('action'), select: (name, o = {}) => ({ name, type: name, ...o }), templateOrType: (name, template, type, o = {}) => field(template ? 'template' : type)(name, o), }; const EditorFactory = { general: (template) => ({ flat: true, fields: { entity: EditorFieldsType.entity('entity', { required: !template }), ...(!template && { attribute: EditorFieldsType.select('attribute', { type: 'attribute', selectorOf: 'entity', showIf: (c) => Boolean(c.entity), }), }), }, }), content: (template) => ({ title: 'editor.title.content', icon: HA_CONTEXT.icons.textShort, fields: { ...(template ? { name: EditorFieldsType.tpl('name'), } : { name: EditorFieldsType.entityName('name', { context: { entity: 'entity' } }), }), toggleName: EditorFieldsType.toggle('hide.name', { invert: true, array: true, showIf: (c) => !is.jinja(c.hide) }), ...(template ? { secondary: EditorFieldsType.tpl('secondary'), toggleSecondaryInfo: EditorFieldsType.toggle('hide.secondary_info', { invert: true, array: true, showIf: (c) => !is.jinja(c.hide) }), percent: EditorFieldsType.tpl('percent'), } : { /* next release bar_max_width: EditorFieldsType.slider('bar_max_width', { target: 'bar_max_width', resolveVirtual: (c) => parseInt(c.bar_max_width) || 0, virtual: true, onVirtualChange: (value, config) => ({ ...config, bar_max_width: value ? `${value}px` : undefined, }), }), */ unit: EditorFieldsType.text('unit', { width: availableSpace(32, 0.2) }), decimal: EditorFieldsType.decimal('decimal', { width: availableSpace(32, 0.2) }), min_value: EditorFieldsType.number('min_value', { width: availableSpace(32, 0.6) }), toggleUnit: EditorFieldsType.toggle('disable_unit', { invert: true }), max_value: EditorFieldsType.number('max_value', { showIf: (c) => typeof c.max_value !== 'string' }), use_max_entity: EditorFieldsType.toggle('use_max_entity', { virtual: true, resolveVirtual: (c) => typeof c.max_value === 'string', onVirtualChange: (value, config) => ({ ...config, max_value: value ? '' : 100, max_value_attribute: value ? config.max_value_attribute : undefined, }), }), max_value_entity: EditorFieldsType.entity('max_value_entity', { target: 'max_value', showIf: (c) => typeof c.max_value === 'string', onClear: (config) => ({ ...config, max_value: 100, max_value_attribute: undefined }), }), max_value_attribute: EditorFieldsType.select('max_value_attribute', { type: 'maxValueAttribute', selectorOf: 'max_value', showIf: (c) => typeof c.max_value === 'string' && c.max_value !== '', }), toggleValue: EditorFieldsType.toggle('hide.value', { invert: true, array: true, showIf: (c) => !is.jinja(c.hide) }), toggleSecondaryInfo: EditorFieldsType.toggle('hide.secondary_info', { invert: true, array: true, showIf: (c) => !is.jinja(c.hide) }), state_content: EditorFieldsType.stateContent('state_content', { context: { filter_entity: 'entity' }, }), }), }, }), theme: (template, badge) => ({ title: 'editor.title.theme', icon: HA_CONTEXT.icons.listBox, fields: { ...(template ? {} : { theme: EditorFieldsType.select('theme') }), icon: EditorFieldsType.templateOrType('icon', template, 'icon', template ? {} : { width: availableSpace() }), color: EditorFieldsType.templateOrType('color', template, 'color', { showIf: (c) => is.nullish(c.theme), ...(template ? {} : { width: availableSpace() }), }), toggleIcon: EditorFieldsType.toggle('hide.icon', { invert: true, array: true, showIf: (c) => !is.jinja(c.hide) }), ...(badge ? {} : { force_circular_background: EditorFieldsType.toggle('force_circular_background'), bar_size: EditorFieldsType.select('bar_size', { width: availableSpace() }), bar_position: EditorFieldsType.select('bar_position', { width: availableSpace() }), bar_single_line: EditorFieldsType.toggle('bar_single_line', { showIf: (c) => c.bar_position === 'overlay' }), text_shadow: EditorFieldsType.toggle('text_shadow', { showIf: (c) => c.bar_position === 'overlay' }), }), bar_color: EditorFieldsType.templateOrType('bar_color', template, 'color', { showIf: (c) => is.nullish(c.theme), ...(template ? {} : { width: availableSpace() }), }), bar_orientation: EditorFieldsType.select('bar_orientation', template ? {} : { width: availableSpace() }), reverse_secondary_info_row: EditorFieldsType.toggle('reverse_secondary_info_row', { showIf: (c) => (!c.bar_position || c.bar_position === 'default') && c.layout === 'horizontal' }), center_zero: EditorFieldsType.toggle('center_zero'), bar_effect: EditorFieldsType.tpl('bar_effect', { showIf: (c) => is.jinja(c.bar_effect) }), toggleBar: EditorFieldsType.toggle('hide.progress_bar', { invert: true, array: true, showIf: (c) => !is.jinja(c.hide) }), ...(badge ? {} : { badge_icon: EditorFieldsType.tpl('badge_icon'), badge_color: EditorFieldsType.tpl('badge_color'), }), hide: EditorFieldsType.tpl('hide', { showIf: (c) => is.jinja(c.hide) }), ...(badge ? {} : { layout: EditorFieldsType.select('layout'), }), }, }), interactions: (badge) => { const fields = [ 'tap_action', 'hold_action', 'double_tap_action', ...(badge ? [] : ['icon_tap_action', 'icon_hold_action', 'icon_double_tap_action']), ]; return { title: 'editor.title.interaction', icon: HA_CONTEXT.icons.gestureTapHold, fields: Object.fromEntries(fields.map((n) => [n, EditorFieldsType.action(n)])), }; }, build: (template, badge) => ({ general: EditorFactory.general(template), content: EditorFactory.content(template), theme: EditorFactory.theme(template, badge), interactions: EditorFactory.interactions(badge), }), }; /****************************************************************************************** * 🛠️ EntityProgressCardEditor * ======================================================================================== */ class EntityProgressCardEditor extends EditorBase { _configHelper = new CardConfigHelper(); static _fields = EditorFactory.build(false, false); } /****************************************************************************************** * 🛠️ EntityProgressBadgeEditor * ======================================================================================== */ class EntityProgressBadgeEditor extends EditorBase { _configHelper = new BadgeConfigHelper(); static _fields = EditorFactory.build(false, true); } /****************************************************************************************** * 🛠️ EntityProgressTemplateEditor * ======================================================================================== */ class EntityProgressTemplateEditor extends EditorBase { _configHelper = new TemplateConfigHelper(); static _fields = EditorFactory.build(true, false); } /****************************************************************************************** * 🛠️ EntityProgressBadgeTemplateEditor * ======================================================================================== */ class EntityProgressBadgeTemplateEditor extends EditorBase { _configHelper = new BadgeTemplateConfigHelper(); static _fields = EditorFactory.build(true, true); } /****************************************************************************************** * 🔧 Register components */ RegistrationHelper.registerCard(META.types.card, EntityProgressCard, EntityProgressCardEditor); RegistrationHelper.registerBadge(META.types.badge, EntityProgressBadge, EntityProgressBadgeEditor); RegistrationHelper.registerCard(META.types.template, EntityProgressTemplateCard, EntityProgressTemplateEditor); RegistrationHelper.registerBadge(META.types.badgeTemplate, EntityProgressTemplateBadge, EntityProgressBadgeTemplateEditor); RegistrationHelper.registerCardFeature(META.types.feature, EntityProgressFeatures); /****************************************************************************************** * 🔧 Show module info */ console.groupCollapsed(CARD.console.message, CARD.console.css); console.log(CARD.console.link); console.groupEnd();