Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the time domain (time entities) to the list of standard configuration actions #808

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/components/timeslot-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { customElement, property, eventOptions } from 'lit/decorators';
import { mdiUnfoldMoreVertical } from '@mdi/js';
import { HomeAssistant } from 'custom-card-helpers';

import { Timeslot, Action, EVariableType, LevelVariable, ListVariable } from '../types';
import { Timeslot, Action, EVariableType, LevelVariable, ListVariable, TimeVariable } from '../types';
import { stringToTime, timeToString, roundTime, parseRelativeTime } from '../data/date-time/time';
import { compareActions } from '../data/actions/compare_actions';
import { levelVariableDisplay } from '../data/variables/level_variable';
import { timeVariableDisplay } from '../data/variables/time_variable';
import { unique, PrettyPrintName, getLocale } from '../helpers';
import { localize } from '../localize/localize';
import { stringToDate } from '../data/date-time/string_to_date';
Expand Down Expand Up @@ -246,6 +247,9 @@ export class TimeslotEditor extends LitElement {
variable = variable as ListVariable;
const listItem = variable.options.find(e => e.value == value);
return PrettyPrintName(listItem && listItem.name ? listItem.name : String(value));
} else if (variable.type == EVariableType.Time) {
variable = variable as TimeVariable;
return timeVariableDisplay(value, variable);
} else return '';
})
.join(', ');
Expand Down
31 changes: 30 additions & 1 deletion src/components/variable-picker.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { LitElement, html, css } from 'lit';
import { property, customElement } from 'lit/decorators.js';
import { Variable, LevelVariable, EVariableType, ListVariable, TextVariable } from '../types';
import { HomeAssistant } from 'custom-card-helpers';
import { Variable, LevelVariable, EVariableType, ListVariable, TextVariable, TimeVariable } from '../types';

import './variable-slider';
import './button-group';
import { fireEvent } from 'custom-card-helpers';

@customElement('scheduler-variable-picker')
export class SchedulerVariablePicker extends LitElement {
@property()
hass?: HomeAssistant;

@property()
variable?: Variable | null;

Expand All @@ -28,6 +32,7 @@ export class SchedulerVariablePicker extends LitElement {
else if (this.variable.type == EVariableType.Level) return this.renderLevelVariable();
else if (this.variable.type == EVariableType.List) return this.renderListVariable();
else if (this.variable.type == EVariableType.Text) return this.renderTextVariable();
else if (this.variable.type == EVariableType.Time) return this.renderTimeVariable();
else return html``;
}

Expand Down Expand Up @@ -83,9 +88,33 @@ export class SchedulerVariablePicker extends LitElement {
`;
}

renderTimeVariable() {
if (!this.hass || !this.variable) {
console.warn(`${this.renderTimeVariable.name}() not rendering: undefined references`);
return html``;
}
const variable = this.variable as TimeVariable;
const value = this.value;

return html`
<ha-time-input
.enableSecond=${variable.enable_seconds}
.value=${value}
.locale=${this.hass.locale}
@value-changed=${this.listVariableUpdated}
></ha-time-input>
<div class="key">${variable.enable_seconds ? 'Hours:Minutes:Seconds' : 'Hours:Minutes'}</div>
`;
Comment on lines +99 to +107
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The choice of the <ha-time-input> component can be argued. My reasoning for not using the <time-picker> component (which is used to set / edit the scheduler time) was simply to visually differentiate between the two time sections (see other comments). Users would be used to the <time-picker> component for setting the scheduler time and they would also be used to the <ha-time-input> component for editing HA time entities.

}

static styles = css`
ha-textfield {
width: 100%;
}
div.key {
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
font-style: italic;
font-size: 0.75rem;
}
`;
}
19 changes: 18 additions & 1 deletion src/data/actions/assign_action.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { Action, EVariableType, LevelVariable, ListVariable, ServiceCall, TextVariable } from '../../types';
import {
Action,
EVariableType,
LevelVariable,
ListVariable,
ServiceCall,
TextVariable,
TimeVariable,
} from '../../types';
import { omit } from '../../helpers';

export const assignAction = (entity_id: string, action: Action) => {
Expand Down Expand Up @@ -32,6 +40,15 @@ export const assignAction = (entity_id: string, action: Action) => {
[key]: config.options.length ? config.options[0].value : undefined,
},
};
} else if (config.type == EVariableType.Time) {
config = config as TimeVariable;
output = {
...output,
service_data: {
...output.service_data,
[key]: config.enable_seconds ? '00:00:00' : '00:00',
},
};
} else if (config.type == EVariableType.Text) {
config = config as TextVariable;
output = {
Expand Down
1 change: 1 addition & 0 deletions src/data/actions/compare_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function compareActions(actionA: Action, actionB: Action, allowVars = fal
if (variable.type === EVariableType.List) {
return (variable as ListVariable).options.some(e => e.value === value);
} else if (variable.type === EVariableType.Level) return !isNaN(value);
else if (variable.type == EVariableType.Time) return true;
else if (variable.type == EVariableType.Text) return true;

return false;
Expand Down
5 changes: 4 additions & 1 deletion src/data/actions/compute_action_display.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { computeEntity } from 'custom-card-helpers';
import { Action, EVariableType, LevelVariable, ListVariable, TextVariable } from '../../types';
import { Action, EVariableType, LevelVariable, ListVariable, TextVariable, TimeVariable } from '../../types';
import { levelVariableDisplay } from '../variables/level_variable';
import { PrettyPrintName } from '../../helpers';
import { listVariableDisplay } from '../variables/list_variable';
import { textVariableDisplay } from '../variables/text_variable';
import { timeVariableDisplay } from '../variables/time_variable';

const wildcardPattern = /\{([^\}]+)\}/;
const parameterPattern = /\[([^\]]+)\]/;
Expand All @@ -26,6 +27,8 @@ export function computeActionDisplay(action: Action) {
replacement = levelVariableDisplay(action.service_data![field], action.variables![field] as LevelVariable);
else if (action.variables![field].type == EVariableType.List)
replacement = listVariableDisplay(action.service_data![field], action.variables![field] as ListVariable);
else if (action.variables![field].type == EVariableType.Time)
replacement = timeVariableDisplay(action.service_data![field], action.variables![field] as TimeVariable);
else replacement = textVariableDisplay(action.service_data![field], action.variables![field] as TextVariable);
} else {
replacement = action.service_data![field];
Expand Down
1 change: 1 addition & 0 deletions src/data/actions/migrate_action_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const migrateActionConfig = (
);
else return null;

case EVariableType.Time:
case EVariableType.Text:
//keep the selected text variable
return output.map(e =>
Expand Down
7 changes: 5 additions & 2 deletions src/data/variables/compute_merged_variable.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { LevelVariable, ListVariable, TextVariable, EVariableType } from '../../types';
import { LevelVariable, ListVariable, TextVariable, TimeVariable, EVariableType } from '../../types';
import { levelVariable } from './level_variable';
import { listVariable } from './list_variable';
import { textVariable } from './text_variable';
import { timeVariable } from './time_variable';
import { isDefined, unique } from '../../helpers';
import { computeVariables } from './compute_variables';

export function computeMergedVariable(
...variables: Partial<LevelVariable | ListVariable | TextVariable>[]
): LevelVariable | ListVariable | TextVariable | undefined {
): LevelVariable | ListVariable | TextVariable | TimeVariable | undefined {
const types = unique(variables.map(e => e.type).filter(isDefined));
if (!types.length) {
variables = Object.values(computeVariables(Object.assign({}, ...variables))!);
Expand All @@ -18,6 +19,8 @@ export function computeMergedVariable(

if (types[0] == EVariableType.Level) {
return levelVariable(...(variables as LevelVariable[]));
} else if (types[0] == EVariableType.Time) {
return timeVariable(...(variables as TimeVariable[]));
} else if (types[0] == EVariableType.List) {
return listVariable(...(variables as ListVariable[]));
} else return textVariable(...(variables as TextVariable[]));
Expand Down
5 changes: 4 additions & 1 deletion src/data/variables/compute_variables.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { LevelVariable, ListVariable, TextVariable, Dictionary, VariableDictionary } from '../../types';
import { LevelVariable, ListVariable, TextVariable, TimeVariable, Dictionary, VariableDictionary } from '../../types';
import { listVariable } from './list_variable';
import { levelVariable } from './level_variable';
import { textVariable } from './text_variable';
import { timeVariable } from './time_variable';

export function computeVariables(
variables?: Dictionary<Partial<LevelVariable | ListVariable | TextVariable>>
Expand All @@ -14,6 +15,8 @@ export function computeVariables(
return [field, listVariable(variable as ListVariable)];
} else if ('min' in variable || 'max' in variable) {
return [field, levelVariable(variable as LevelVariable)];
} else if ('enable_seconds' in variable) {
return [field, timeVariable(variable as TimeVariable)];
} else {
return [field, textVariable(variable as TextVariable)];
}
Expand Down
19 changes: 19 additions & 0 deletions src/data/variables/time_variable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { isDefined } from '../../helpers';
import { EVariableType, TimeVariable } from '../../types';

export function timeVariable(...config: Partial<TimeVariable>[]) {
//factory function to create TimeVariable from configuration

const name = config.map(e => e.name).filter(isDefined);

const variable: TimeVariable = {
type: EVariableType.Time,
name: name.length ? name.reduce((_acc, val) => val) : undefined,
enable_seconds: config.some(e => e.enable_seconds),
};
return variable;
}

export function timeVariableDisplay(value: any, _variable: TimeVariable): string {
return String(value);
}
1 change: 1 addition & 0 deletions src/editor/scheduler-editor-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,7 @@ export class SchedulerEditorOptions extends LitElement {

<div class="header">${this.hass.localize('ui.panel.config.automation.editor.conditions.type.state.label')}</div>
<scheduler-variable-picker
.hass=${this.hass}
.variable=${states}
.value=${this.conditionValue}
@value-changed=${(ev: CustomEvent) => (this.conditionValue = ev.detail.value)}
Expand Down
28 changes: 19 additions & 9 deletions src/editor/scheduler-editor-time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ export class SchedulerEditorTime extends LitElement {

render() {
if (!this.hass || !this.config || !this.entities || !this.actions) return html``;
let timePickerHeader = '';
if (!this.timeslots) {
timePickerHeader = this.hass.localize('ui.dialogs.helper_settings.input_datetime.time');
if (this.entities?.[0].id.startsWith('time'))
timePickerHeader += ` (${localize('ui.panel.common.title', getLocale(this.hass))})`;
}
Comment on lines +96 to +101
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This timePickerHeader logic changes the section title from "TIME" to "TIME (SCHEDULER)" to help disambiguate between the two time sections — see red arrows in the screenshots below. This only applies when the selected entity is a time entity. If the user selected a different entity, say switch, the section title remains as before, i.e. simply "TIME".

Without the timePickerHeader logic:
New Schedule - Time Editor - ambiguous highlight

With the timePickerHeader logic:
New Schedule - Time Editor - highlight

return html`
<div class="content">
<div class="header">
Expand All @@ -102,7 +108,7 @@ export class SchedulerEditorTime extends LitElement {
${!this.timeslots
? html`
${this.getVariableEditor()} ${this.renderDays()}
<div class="header">${this.hass.localize('ui.dialogs.helper_settings.input_datetime.time')}</div>
<div class="header">${timePickerHeader}</div>
<time-picker
.hass=${this.hass}
.value=${this.schedule.timeslots[0].start}
Expand Down Expand Up @@ -150,6 +156,10 @@ export class SchedulerEditorTime extends LitElement {
`;
}

getEntityName(entity: EntityElement) {
return entity.name || this.hass!.states[entity.id].attributes.friendly_name || computeEntity(entity.id);
}

renderSummary() {
if (!this.entities || !this.actions) return html``;
return html`
Expand All @@ -159,11 +169,7 @@ export class SchedulerEditorTime extends LitElement {
entity => html`
<div>
<ha-icon icon="${PrettyPrintIcon(entity.icon)}"> </ha-icon>
${capitalize(
PrettyPrintName(
entity.name || this.hass!.states[entity.id].attributes.friendly_name || computeEntity(entity.id)
)
)}
${capitalize(PrettyPrintName(this.getEntityName(entity)))}
</div>
`
)}
Expand Down Expand Up @@ -395,11 +401,15 @@ export class SchedulerEditorTime extends LitElement {
return actions.map(action => {
return Object.entries(this.actions!.find(e => compareActions(e, action, true))!.variables!).map(
([field, variable]) => {
let header: string = variable.name || PrettyPrintName(field);
if (header.toLowerCase() === 'time') {
const entity = this.entities?.[0];
if (entity) header += ` (${PrettyPrintName(this.getEntityName(entity))})`;
}
Comment on lines +404 to +408
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This header logic changes the section title from "TIME" to "TIME (<entity name>)" to help disambiguate between the two time sections — see red arrows in the screenshots below. This only applies when the selected entity is a time entity. If the user selected a different entity, say switch, the section title remains as before (not including the entity name).

Without the header logic:
New Schedule - Time Editor - ambiguous highlight

With the header logic:
New Schedule - Time Editor - highlight

return html`
<div class="header">
${variable.name || PrettyPrintName(field)}
</div>
<div class="header">${header}</div>
<scheduler-variable-picker
.hass=${this.hass}
.variable=${variable}
.value=${action.service_data ? action.service_data[field] : null}
@value-changed=${(ev: CustomEvent) =>
Expand Down
4 changes: 4 additions & 0 deletions src/localize/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@
"script": {
"script": "execute"
},
"time": {
"set_value": "set value[ to {time}]"
},
"vacuum": {
"start_pause": "start / pause"
},
Expand All @@ -76,6 +79,7 @@
"media_player": "media players",
"notify": "notification",
"switch": "switches",
"time": "time",
"vacuum": "vacuums",
"water_heater": "water heaters"
},
Expand Down
3 changes: 3 additions & 0 deletions src/standard-configuration/action_icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ const actionIcons: IconList = {
turn_on: 'mdi:flash',
turn_off: 'mdi:flash-off',
},
time: {
set_value: 'mdi:clock-outline',
},
vacuum: {
turn_on: 'mdi:power',
start: 'mdi:play-circle-outline',
Expand Down
3 changes: 3 additions & 0 deletions src/standard-configuration/action_name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ const actionNamesList: Record<string, Record<string, string | actionNameTemplate
turn_on: 'ui.card.vacuum.actions.turn_on',
turn_off: 'ui.card.vacuum.actions.turn_off',
},
time: {
set_value: 'services.time.set_value',
},
vacuum: {
turn_on: 'ui.card.vacuum.actions.turn_on',
start: 'ui.card.vacuum.start_cleaning',
Expand Down
9 changes: 9 additions & 0 deletions src/standard-configuration/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,15 @@ export const actionList: Record<string, Record<string, ActionItem>> = {
turn_on: {},
turn_off: {},
},
time: {
set_value: {
variables: {
time: {
enable_seconds: true,
},
},
},
},
vacuum: {
turn_on: { supported_feature: 1 },
start: {
Expand Down
3 changes: 3 additions & 0 deletions src/standard-configuration/standardActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { HassEntity } from 'home-assistant-js-websocket';
import { levelVariable } from '../data/variables/level_variable';
import { listVariable } from '../data/variables/list_variable';
import { textVariable } from '../data/variables/text_variable';
import { timeVariable } from '../data/variables/time_variable';
import { actionName } from './action_name';
import { actionIcon } from './action_icons';
import { getVariableName } from './variable_name';
Expand Down Expand Up @@ -124,6 +125,8 @@ const parseActionVariable = (
return listVariable(config);
} else if ('min' in config && isDefined(config.min) && 'max' in config && isDefined(config.max)) {
return levelVariable(config);
} else if ('enable_seconds' in config && isDefined(config.enable_seconds)) {
return timeVariable(config);
} else {
return textVariable(config);
}
Expand Down
1 change: 1 addition & 0 deletions src/standard-configuration/standardIcon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const domainIcons: Record<string, string> = {
sensor: 'mdi:eye',
sun: 'mdi:white-balance-sunny',
switch: 'mdi:flash',
time: 'mdi:clock',
timer: 'mdi:timer',
vacuum: 'mdi:robot-vacuum',
water_heater: 'mdi:water-boiler',
Expand Down
3 changes: 3 additions & 0 deletions src/standard-configuration/standardStates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { DefaultActionIcon } from '../const';
import { levelVariable } from '../data/variables/level_variable';
import { listVariable } from '../data/variables/list_variable';
import { textVariable } from '../data/variables/text_variable';
import { timeVariable } from '../data/variables/time_variable';
import { getLocale, isDefined } from '../helpers';
import { localize } from '../localize/localize';
import { Variable } from '../types';
Expand Down Expand Up @@ -43,6 +44,8 @@ export function standardStates(entity_id: string, hass: HomeAssistant): Variable
return listVariable(stateConfig);
} else if ('min' in stateConfig && isDefined(stateConfig.min) && 'max' in stateConfig && isDefined(stateConfig.max)) {
return levelVariable(stateConfig);
} else if ('enable_seconds' in stateConfig && isDefined(stateConfig.enable_seconds)) {
return timeVariable(stateConfig);
} else {
return textVariable(stateConfig);
}
Expand Down
Loading
Loading