openEntityPopover(entityId) : null}"
>
${o(appendUnit(value, hasConfiguredUnit ? unit : false))}
`;
}
else if (type === 'relativetime') {
valueCell = b `
openEntityPopover(state.entity_id)
: null}"
>
`;
}
else if (isToggleEntity) {
valueCell = b `
openEntityPopover(state.entity_id)
: null}"
>
${appendUnit(value, configuredUnit, value)}
`;
}
}
else {
let value = typeof decimals === 'number'
? formatNumber(state, {
decimals,
locale: hass.locale,
})
: state;
valueCell = b ` openEntityPopover(entityId) : null}
>
${appendUnit(value, hasConfiguredUnit ? unit : false)}
`;
}
if (heading === false) {
return valueCell;
}
const tooltip = heading || entityTooltip;
const headingClasses = [
'entity-heading',
canOpenEntity && 'clickable',
isToggleEntity && 'toggle-entity',
entityDomain && `domain-${safeClass(entityDomain)}`,
entityState && `state-${safeClass(entityState)}`,
isToggleEntity &&
getToggleKindClass(getToggleKind({
icon: renderedIcon || state?.attributes?.icon,
label: typeof heading === 'string'
? heading
: state?.attributes?.friendly_name,
entity: state,
hass,
})),
]
.filter(Boolean)
.join(' ');
const headingResult = renderedIcon
? b `
${showHeading ? b `
${title}
` : ''}
${list.map(({ value, icon, iconConfigured, name, hide_when_off }) => {
if (hide_when_off === true && state === HVAC_MODES.OFF)
return A;
const modeClass = safeClass(value);
const displayName = maybeRenderName(name, value);
const tooltip = displayName ? A : controlTooltip || A;
return b `
setMode(type, value)}
@keydown=${(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setMode(type, value);
}
}}
>
${maybeRenderIcon(icon, iconConfigured)}
${displayName
? b `${displayName}`
: null}
`;
})}
`;
}
function parseSetpoints(setpoints, attributes, adapter = climateAdapter, entityState) {
if (setpoints === false) {
return {};
}
if (setpoints) {
return Object.entries(setpoints).reduce((result, [name, sp]) => {
if (sp?.hide)
return result;
const hiddenStates = Array.isArray(sp?.hide_when)
? sp.hide_when
: sp?.hide_when
? [sp.hide_when]
: [];
if (entityState && hiddenStates.includes(entityState))
return result;
result[name] = attributes?.[name];
return result;
}, {});
}
return adapter.getSetpoints(attributes);
}
function parseService(config, adapter = climateAdapter) {
if (!config) {
return adapter.getSetpointService();
}
return config;
}
const SETPOINT_DEBOUNCE_TIMEOUT = 500;
const STEP_SIZE = 0.5;
const DECIMALS = 1;
const UPDATING_TIMEOUT = 10000;
const MODE_TYPES = Object.values(MODES);
const ICONS = {
UP: 'hass:chevron-up',
DOWN: 'hass:chevron-down',
PLUS: 'mdi:plus',
MINUS: 'mdi:minus',
};
const DEFAULT_HIDE = {
temperature: false,
state: false,
};
const CONTROL_ORDER = [
MODES.PRESET,
MODES.FAN,
MODES.HVAC,
MODES.SWING,
MODES.SWING_HORIZONTAL,
MODES.SWING_VERTICAL,
MODES.VANE_HORIZONTAL,
MODES.VANE_VERTICAL,
MODES.DIRECTION,
MODES.OSCILLATING,
MODES.STATE,
];
const CONTROL_METADATA_KEYS = ['entity', 'hide_when_off', 'hide_off_when_off'];
function getConfiguredEntities(config) {
return config.entities ?? [];
}
function shouldShowModeControl(type, modeOption, config) {
const modeKey = String(modeOption);
const configuredMode = getConfiguredModeValue(modeKey, config);
if (isModeValue(configuredMode)) {
return configuredMode.include !== false;
}
const hasExplicitConfig = Object.keys(config).some((key) => !key.startsWith('_'));
const hideUnlistedModes = type === MODES.PRESET;
return configuredMode ?? !(hideUnlistedModes && hasExplicitConfig);
}
function normalizeModeConfigKey(value) {
return value.toLowerCase().replace(/\s+/g, '_');
}
function getConfiguredModeValue(modeKey, specification) {
const normalizedModeKey = normalizeModeConfigKey(modeKey);
const exactValue = specification[modeKey];
if (typeof exactValue !== 'undefined')
return exactValue;
if (typeof specification[normalizedModeKey] !== 'undefined') {
return specification[normalizedModeKey];
}
const matchingEntry = Object.entries(specification).find(([key]) => !key.startsWith('_') &&
!CONTROL_METADATA_KEYS.includes(key) &&
normalizeModeConfigKey(key) === normalizedModeKey);
return matchingEntry?.[1];
}
function isModeValue(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function getOrderedModeOptions(modeOptions, specification) {
const configuredKeys = Array.isArray(specification._order)
? specification._order.map(String)
: Object.keys(specification).filter((key) => !key.startsWith('_') && !CONTROL_METADATA_KEYS.includes(key));
if (configuredKeys.length === 0)
return modeOptions;
const optionsByKey = new Map();
modeOptions.forEach((modeOption) => {
optionsByKey.set(normalizeModeConfigKey(String(modeOption)), modeOption);
});
const usedKeys = new Set();
const orderedOptions = [];
configuredKeys.forEach((key) => {
const normalizedKey = normalizeModeConfigKey(key);
if (!optionsByKey.has(normalizedKey) || usedKeys.has(normalizedKey))
return;
orderedOptions.push(optionsByKey.get(normalizedKey));
usedKeys.add(normalizedKey);
});
modeOptions.forEach((modeOption) => {
const normalizedKey = normalizeModeConfigKey(String(modeOption));
if (usedKeys.has(normalizedKey))
return;
orderedOptions.push(modeOption);
usedKeys.add(normalizedKey);
});
return orderedOptions;
}
function getModeList(type, attributes, adapter, specification = {}) {
let modeOptions = attributes[adapter.getModeAttribute(type)];
if (type === MODES.STATE) {
modeOptions = ['off', 'on'];
}
else if (type === MODES.DIRECTION && attributes.direction) {
modeOptions = ['forward', 'reverse'];
}
else if (type === MODES.OSCILLATING &&
typeof attributes.oscillating === 'boolean') {
modeOptions = [false, true];
}
if (!Array.isArray(modeOptions)) {
return [];
}
return getOrderedModeOptions(modeOptions, specification)
.filter((modeOption) => shouldShowModeControl(type, modeOption, specification))
.map((modeOption) => {
const modeKey = String(modeOption);
const configuredMode = getConfiguredModeValue(modeKey, specification);
const values = isModeValue(configuredMode)
? configuredMode
: {};
const { name: configuredName, ...modeValues } = values;
const hideWhenOff = values.hide_when_off === true ||
(specification.hide_off_when_off === true &&
normalizeModeConfigKey(modeKey) === 'off');
const name = configuredName === false
? false
: typeof configuredName === 'string'
? configuredName
: getModeName(modeKey);
return {
...modeValues,
hide_when_off: hideWhenOff || undefined,
icon: values.icon ??
(type === MODES.FAN
? getFanModeIcon(modeKey, modeOptions)
: undefined) ??
getModeIcon(modeKey),
iconConfigured: typeof values.icon !== 'undefined',
value: modeKey,
name,
};
});
}
function isSelectModeEntity(stateObj) {
return (typeof stateObj?.entity_id === 'string' &&
stateObj.entity_id.startsWith('select.') &&
Array.isArray(stateObj.attributes?.options));
}
function getModeListFromSelect(stateObj, specification = {}) {
const modeOptions = stateObj.attributes.options;
return getOrderedModeOptions(modeOptions, specification)
.filter((modeOption) => shouldShowModeControl('select', modeOption, specification))
.map((modeOption) => {
const modeKey = String(modeOption);
const configuredMode = getConfiguredModeValue(modeKey, specification);
const values = isModeValue(configuredMode)
? configuredMode
: {};
const { name: configuredName, ...modeValues } = values;
const hideWhenOff = values.hide_when_off === true ||
(specification.hide_off_when_off === true &&
normalizeModeConfigKey(modeKey) === 'off');
const name = configuredName === false
? false
: typeof configuredName === 'string'
? configuredName
: getModeName(modeKey);
return {
...modeValues,
hide_when_off: hideWhenOff || undefined,
icon: values.icon ?? getModeIcon(modeKey),
iconConfigured: typeof values.icon !== 'undefined',
value: modeKey,
name,
};
});
}
function getCardStyle(entityDomain, attributes) {
if (entityDomain !== 'fan')
return '';
const percentage = Number(attributes?.percentage);
if (Number.isNaN(percentage))
return '';
const normalizedPercentage = Math.min(Math.max(percentage, 0), 100);
const fanSpinDuration = Math.max(0.9, 3.2 - (normalizedPercentage / 100) * 2.1);
return `--st-fan-spin-duration: ${fanSpinDuration.toFixed(2)}s;`;
}
function getCardModSurfaceDeclarations(cardMod) {
const style = cardMod?.style;
if (typeof style === 'object' && style) {
const cardStyle = style['ha-card'];
if (typeof cardStyle === 'string')
return cardStyle.trim();
const rootStyle = style['.'];
if (typeof rootStyle === 'string') {
const rootMatch = rootStyle.match(/ha-card\s*\{([\s\S]*?)\}/);
return (rootMatch?.[1] ?? '').trim();
}
}
if (typeof style !== 'string')
return '';
const match = style.match(/ha-card\s*\{([\s\S]*?)\}/);
return (match?.[1] ?? '').trim();
}
function getInlineCardStyle(config, entityDomain, attributes) {
return [
getCardModSurfaceDeclarations(config.card_mod),
getCardStyle(entityDomain, attributes),
]
.filter((style) => !!style)
.join('; ');
}
function supportsModeType(type, entityDomain, attributes, adapter) {
return (MODE_TYPES.includes(type) &&
(type === MODES.STATE
? entityDomain === 'fan' || entityDomain === 'humidifier'
: typeof attributes[adapter.getModeAttribute(type)] !== 'undefined'));
}
function buildBasicControlModes(items, entityDomain, attributes, adapter) {
return items
.filter((type) => supportsModeType(type, entityDomain, attributes, adapter))
.map((type) => ({
type,
hide_when_off: false,
list: getModeList(type, attributes, adapter),
}));
}
function buildConfiguredControlModes(config, entityDomain, attributes, adapter, hass) {
if (config.control === false)
return [];
if (Array.isArray(config.control)) {
return buildBasicControlModes(config.control, entityDomain, attributes, adapter);
}
if (config.control && typeof config.control === 'object') {
const controlConfig = config.control;
const configuredEntries = Object.entries(config.control).filter(([type]) => !type.startsWith('_'));
const configuredOrder = Array.isArray(controlConfig._order)
? controlConfig._order.map(String)
: undefined;
const orderedTypes = configuredOrder
? [
...configuredOrder.filter((type) => configuredEntries.some(([entryType]) => entryType === type)),
...configuredEntries
.map(([type]) => type)
.filter((type) => !configuredOrder.includes(type)),
]
: configuredEntries.map(([type]) => type);
const entries = orderedTypes.map((type) => [type, controlConfig[type]]);
if (entries.length > 0) {
return entries
.filter(([, definition]) => definition !== false)
.filter(([type, definition]) => {
const controlEntity = definition === true || definition === false
? undefined
: definition.entity;
const selectState = controlEntity
? hass?.states?.[controlEntity]
: undefined;
return (isSelectModeEntity(selectState) ||
supportsModeType(type, entityDomain, attributes, adapter));
})
.map(([type, definition]) => {
const { _name, _hide_when_off, hide_when_off, hide_off_when_off, _icons, _heading, entity: controlEntity, ...controlField } = definition === true ? {} : definition;
const selectState = controlEntity
? hass?.states?.[controlEntity]
: undefined;
const useSelectEntity = isSelectModeEntity(selectState);
const modeSpecification = {
...controlField,
...(hide_off_when_off === true ? { hide_off_when_off } : {}),
};
return {
type,
entity: useSelectEntity ? controlEntity : undefined,
hide_when_off: hide_when_off ?? _hide_when_off,
icons: _icons,
heading: _heading,
name: _name,
preserve_option_order: Object.keys(controlField).length > 0,
list: useSelectEntity
? getModeListFromSelect(selectState, modeSpecification)
: getModeList(type, attributes, adapter, modeSpecification),
};
});
}
}
return buildBasicControlModes(adapter.getDefaultControl(), entityDomain, attributes, adapter);
}
function removeOffFromSecondaryModes(controlModes) {
if (!controlModes.some(({ type }) => type === MODES.STATE)) {
return controlModes;
}
return controlModes.map((mode) => mode.type && mode.type !== MODES.STATE
? {
...mode,
list: mode.list?.filter(({ value }) => value !== 'off') ?? [],
}
: mode);
}
function sortControlModes(controlModes, entityDomain) {
if (entityDomain !== 'fan' && entityDomain !== 'climate')
return controlModes;
const getControlOrder = (type) => {
const index = CONTROL_ORDER.indexOf(type);
return index === -1 ? CONTROL_ORDER.length : index;
};
return [...controlModes].sort((a, b) => getControlOrder(a.type) - getControlOrder(b.type));
}
function shouldPreserveConfiguredControlOrder(control) {
if (Array.isArray(control))
return true;
if (!control || typeof control !== 'object')
return false;
return Object.keys(control).length > 0;
}
class SimpleThermostat extends i$1 {
constructor() {
super(...arguments);
this.modes = [];
this._hass = {};
this.entities = [];
this.showEntities = true;
this.name = '';
this.stepSize = STEP_SIZE;
this._values = {};
this._updatingValues = false;
this._hide = DEFAULT_HIDE;
this._updatingValuesTimeout = null;
this._holdTimer = null;
this._holdFired = false;
this._clickCount = 0;
this._clickTimer = null;
this._setpointUpdateTimer = null;
this._pendingSetpointValues = null;
this._setpointDebounce = SETPOINT_DEBOUNCE_TIMEOUT;
this.localize = (label, prefix = '') => {
const key = `${prefix}${label}`;
return this._hass.localize?.(key) || label;
};
this.toggleEntityChanged = (ev, entityId) => {
if (!this.header || !entityId)
return;
const el = ev.target;
this._callAction(`homeassistant.turn_${el.checked ? 'on' : 'off'}`, {
entity_id: entityId,
});
};
this.setMode = (type, mode) => {
if (type && mode) {
const adapter = getAdapter(this.config.entity);
if (type === MODES.STATE) {
this._callAction(`${adapter.getLocalizationDomain()}.turn_${mode}`, {
entity_id: this.config.entity,
});
fireEvent(this, 'haptic', 'light');
return;
}
const configuredMode = this.modes.find((mode) => mode.type === type);
if (configuredMode?.entity) {
this._callAction('select.select_option', {
entity_id: configuredMode.entity,
option: mode,
});
fireEvent(this, 'haptic', 'light');
return;
}
const value = adapter.transformModePayloadValue?.(type, mode) ?? mode;
this._callAction(`${adapter.getLocalizationDomain()}.${adapter.getModeService(type)}`, {
entity_id: this.config.entity,
[adapter.getModePayloadKey(type)]: value,
});
fireEvent(this, 'haptic', 'light');
}
else {
fireEvent(this, 'haptic', 'failure');
}
};
this.openEntityPopover = (entityId = null) => {
fireEvent(this, 'hass-more-info', {
entityId: entityId || this.config.entity,
});
};
this._onActionPointerDown = (e) => {
if (e.button !== 0 && e.pointerType === 'mouse')
return;
this._holdFired = false;
if (this._holdTimer)
clearTimeout(this._holdTimer);
this._holdTimer = setTimeout(() => {
this._holdFired = true;
this._holdTimer = null;
this._dispatchAction('hold');
}, SimpleThermostat.HOLD_MS);
};
this._onActionPointerUp = () => {
if (this._holdTimer) {
clearTimeout(this._holdTimer);
this._holdTimer = null;
}
};
this._onActionClick = (e) => {
e.preventDefault();
if (this._holdFired) {
this._holdFired = false;
return;
}
this._clickCount += 1;
if (this._clickCount === 1) {
if (this._clickTimer)
clearTimeout(this._clickTimer);
this._clickTimer = setTimeout(() => {
this._clickCount = 0;
this._clickTimer = null;
this._dispatchSetpointTap();
}, SimpleThermostat.DOUBLE_TAP_MS);
}
else {
if (this._clickTimer)
clearTimeout(this._clickTimer);
this._clickTimer = null;
this._clickCount = 0;
this._dispatchAction('double_tap');
}
};
this._onSetpointKeyDown = (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this._dispatchSetpointTap();
}
};
}
static get styles() {
return css_248z;
}
_sendSetpointValues(values) {
const { domain, service, data = {} } = this.service;
this._callAction(`${domain}.${service}`, {
entity_id: this.config.entity,
...data,
...values,
});
}
_scheduleSetpointValues(values) {
const wait = this._setpointDebounce;
if (wait <= 0) {
this._sendSetpointValues(values);
return;
}
this._pendingSetpointValues = { ...values };
if (this._setpointUpdateTimer) {
clearTimeout(this._setpointUpdateTimer);
}
this._setpointUpdateTimer = setTimeout(() => {
const pendingValues = this._pendingSetpointValues;
this._setpointUpdateTimer = null;
this._pendingSetpointValues = null;
if (pendingValues) {
this._sendSetpointValues(pendingValues);
}
}, wait);
}
_getSetpointDebounce(config) {
const value = Number(config?.setpoint_debounce_ms ?? SETPOINT_DEBOUNCE_TIMEOUT);
return Number.isFinite(value) && value >= 0
? value
: SETPOINT_DEBOUNCE_TIMEOUT;
}
_callAction(action, data) {
if (typeof this._hass.callService === 'function') {
const [domain, service] = action.split('.');
this._hass.callService(domain, service, data);
}
else if (typeof this._hass.performAction === 'function') {
this._hass.performAction({ action, data });
}
}
static getConfigElement() {
return window.document.createElement(`${name}-editor`);
}
static getStubConfig(hass) {
const entity = Object.keys(hass.states ?? {}).find((id) => id.startsWith('climate.') ||
id.startsWith('fan.') ||
id.startsWith('humidifier.'));
return { entity: entity ?? '' };
}
setConfig(config) {
this.config = normalizeConfig({
decimals: DECIMALS,
...config,
});
const setpointDebounce = this._getSetpointDebounce(this.config);
if (setpointDebounce !== this._setpointDebounce) {
this._setpointDebounce = setpointDebounce;
}
this.entities = [];
this.showEntities = true;
this.toggleAttribute('embedded', this.config.embedded === true);
if (this._hass?.states) {
this.updateFromHass(this._hass);
}
}
set hass(hass) {
if (hass?.states) {
this._hass = hass;
}
if (!this.config?.entity) {
return;
}
if (!hass?.states) {
return;
}
const entity = hass.states[this.config.entity];
if (!entity) {
return;
}
this.updateFromHass(hass);
}
disconnectedCallback() {
if (this._updatingValuesTimeout) {
clearTimeout(this._updatingValuesTimeout);
this._updatingValuesTimeout = null;
}
if (this._holdTimer) {
clearTimeout(this._holdTimer);
this._holdTimer = null;
}
if (this._clickTimer) {
clearTimeout(this._clickTimer);
this._clickTimer = null;
}
if (this._setpointUpdateTimer) {
clearTimeout(this._setpointUpdateTimer);
this._setpointUpdateTimer = null;
}
this._pendingSetpointValues = null;
super.disconnectedCallback();
}
updateFromHass(hass) {
const entity = hass.states[this.config.entity];
if (this.entity !== entity) {
this.entity = entity;
}
const adapter = getAdapter(this.config.entity);
this.header = parseHeaderConfig(this.config.header, entity, hass, this.config.enhanced_visuals !== false);
this.service = parseService(this.config?.service ?? false, adapter);
const attributes = entity.attributes;
let values = parseSetpoints(this.config?.setpoints ?? null, attributes, adapter, entity.state);
if (this._updatingValues && isEqual(values, this._values)) {
this._updatingValues = false;
if (this._updatingValuesTimeout) {
clearTimeout(this._updatingValuesTimeout);
this._updatingValuesTimeout = null;
}
}
else if (!this._updatingValues) {
this._values = values;
}
const entityDomain = this.config.entity.split('.')[0];
const configuredControlModes = removeOffFromSecondaryModes(buildConfiguredControlModes(this.config, entityDomain, attributes, adapter, hass));
const controlModes = shouldPreserveConfiguredControlOrder(this.config.control)
? configuredControlModes
: sortControlModes(configuredControlModes, entityDomain);
this.modes = controlModes.map((values) => {
const list = values.preserve_option_order
? values.list
: values.type === MODES.HVAC
? sortHvacModes(values.list)
: values.type === MODES.FAN
? sortFanModes(values.list)
: values.list;
const mode = values.entity && hass.states?.[values.entity]
? hass.states[values.entity].state
: values.type === MODES.HVAC || values.type === MODES.STATE
? entity.state
: attributes[adapter.getModePayloadKey(values.type)];
return { ...values, list, mode };
});
const { step: rangeStep } = adapter.getRange(attributes);
this.stepSize = Number(this.config.step_size ?? rangeStep ?? STEP_SIZE);
this._hide = { ...DEFAULT_HIDE, ...(this.config.hide ?? {}) };
const configuredEntities = getConfiguredEntities(this.config);
if (configuredEntities === false) {
this.showEntities = false;
this.entities = [];
}
else if (configuredEntities) {
this.showEntities = true;
this.entities = configuredEntities.map(({ name, entity, attribute, template, unit = '', ...rest }) => {
let state;
const names = [name];
if (entity) {
state = hass.states[entity];
names.push(state?.attributes?.friendly_name);
if (attribute && !template) {
state = state?.attributes?.[attribute];
}
}
else if (attribute && attribute in (this.entity.attributes ?? {})) {
state = template ? this.entity : this.entity.attributes[attribute];
names.push(attribute);
}
names.push(entity);
return {
...rest,
name: names.find((n) => !!n),
state,
entity,
attribute,
template,
unit,
};
});
}
else {
this.showEntities = true;
this.entities = [];
}
}
render({ _hide, _values, _updatingValues, config, entity } = this) {
if (!config) {
return b `
${row
? b `${decreaseButton}${valueButton}${increaseButton}`
: b `${increaseButton}${valueButton}${decreaseButton}`}
${label}
`;
}
renderSetpointLabel({ field }) {
if (this.config.hide?.setpoint_label === true)
return A;
const configuredLabel = this.config.label?.setpoint;
const label = configuredLabel ??
this._hass.localize?.(`ui.card.${getAdapter(this.config.entity).getLocalizationDomain()}.target`) ??
this._hass.localize?.('ui.card.climate.target_temperature') ??
this.localize(field, 'state_attributes.climate.');
return b `