-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Add visibility condition for lovelace cards based on an entity's last updated time #25864
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
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,8 +4,11 @@ import { listenMediaQuery } from "../../../common/dom/media_query"; | |
import { isValidEntityId } from "../../../common/entity/valid_entity_id"; | ||
import { UNKNOWN } from "../../../data/entity"; | ||
import type { HomeAssistant } from "../../../types"; | ||
import { createDurationData } from "../../../common/datetime/create_duration_data"; | ||
import type { HaDurationData } from "../../../components/ha-duration-input"; | ||
|
||
export type Condition = | ||
| LastUpdatedStateCondition | ||
| NumericStateCondition | ||
| StateCondition | ||
| ScreenCondition | ||
|
@@ -24,6 +27,13 @@ interface BaseCondition { | |
condition: string; | ||
} | ||
|
||
export interface LastUpdatedStateCondition extends BaseCondition { | ||
condition: "last_updated_state"; | ||
entity?: string; | ||
within?: HaDurationData; | ||
after?: HaDurationData; | ||
} | ||
|
||
export interface NumericStateCondition extends BaseCondition { | ||
condition: "numeric_state"; | ||
entity?: string; | ||
|
@@ -132,6 +142,62 @@ function checkStateNumericCondition( | |
); | ||
} | ||
|
||
function checkLastUpdatedStateCondition( | ||
condition: LastUpdatedStateCondition, | ||
hass: HomeAssistant | ||
) { | ||
const state_last_changed = ( | ||
condition.entity ? hass.states[condition.entity] : undefined | ||
)?.last_changed; | ||
const within = condition.within; | ||
const after = condition.after; | ||
|
||
function HaDurationData_to_milliseconds( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As mentioned: I feel like this function must exist somewhere, but if it doesn't, this is not the right place for it. Any tips for where it should go? |
||
duration: HaDurationData | undefined | ||
) { | ||
// This function should not be here, and surely something like this already exists? | ||
// If so, I can't find it :'( | ||
if (duration) { | ||
const days = duration.days || 0; | ||
let hours = duration.hours || 0; | ||
let minutes = duration.minutes || 0; | ||
let seconds = duration.seconds || 0; | ||
let milliseconds = duration.milliseconds || 0; | ||
|
||
hours += days * 24; | ||
minutes += hours * 60; | ||
seconds += minutes * 60; | ||
milliseconds += seconds * 1000; | ||
|
||
return milliseconds; | ||
} | ||
return 0; // this also is probably not good | ||
} | ||
|
||
const withinDuration = HaDurationData_to_milliseconds( | ||
createDurationData(within) | ||
); | ||
const afterDuration = HaDurationData_to_milliseconds( | ||
createDurationData(after) | ||
); | ||
|
||
const numericLastUpdatedState = new Date(state_last_changed).getTime(); | ||
const numericWithin = numericLastUpdatedState + withinDuration; | ||
const numericAfter = numericLastUpdatedState + afterDuration; | ||
|
||
if (isNaN(numericLastUpdatedState)) { | ||
return false; | ||
} | ||
|
||
const now = new Date().getTime(); | ||
return ( | ||
(condition.within == null || | ||
isNaN(numericWithin) || | ||
now <= numericWithin) && | ||
(condition.after == null || isNaN(numericAfter) || now > numericAfter) | ||
); | ||
} | ||
|
||
function checkScreenCondition(condition: ScreenCondition, _: HomeAssistant) { | ||
return condition.media_query | ||
? matchMedia(condition.media_query).matches | ||
|
@@ -173,6 +239,8 @@ export function checkConditionsMet( | |
return checkUserCondition(c, hass); | ||
case "numeric_state": | ||
return checkStateNumericCondition(c, hass); | ||
case "last_updated_state": | ||
return checkLastUpdatedStateCondition(c, hass); | ||
case "and": | ||
return checkAndCondition(c, hass); | ||
case "or": | ||
|
@@ -206,6 +274,22 @@ export function extractConditionEntityIds( | |
) { | ||
entityIds.add(condition.below); | ||
} | ||
} else if (condition.condition === "last_updated_state") { | ||
if (condition.entity) { | ||
entityIds.add(condition.entity); | ||
} | ||
if ( | ||
typeof condition.within === "string" && | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not really sure if this chunk is right..., I copied it from the NumericState condition |
||
isValidEntityId(condition.within) | ||
) { | ||
entityIds.add(condition.within); | ||
} | ||
if ( | ||
typeof condition.after === "string" && | ||
isValidEntityId(condition.after) | ||
) { | ||
entityIds.add(condition.after); | ||
} | ||
} else if (condition.condition === "state") { | ||
if (condition.entity) { | ||
entityIds.add(condition.entity); | ||
|
@@ -257,6 +341,14 @@ function validateNumericStateCondition(condition: NumericStateCondition) { | |
(condition.above != null || condition.below != null) | ||
); | ||
} | ||
function validateLastUpdatedStateCondition( | ||
condition: LastUpdatedStateCondition | ||
) { | ||
return ( | ||
condition.entity != null && | ||
(condition.within != null || condition.after != null) | ||
); | ||
} | ||
/** | ||
* Validate the conditions config for the UI | ||
* @param conditions conditions to apply | ||
|
@@ -274,6 +366,8 @@ export function validateConditionalConfig( | |
return validateUserCondition(c); | ||
case "numeric_state": | ||
return validateNumericStateCondition(c); | ||
case "last_updated_state": | ||
return validateLastUpdatedStateCondition(c); | ||
case "and": | ||
return validateAndCondition(c); | ||
case "or": | ||
|
@@ -307,7 +401,8 @@ export function addEntityToCondition( | |
|
||
if ( | ||
condition.condition === "state" || | ||
condition.condition === "numeric_state" | ||
condition.condition === "numeric_state" || | ||
condition.condition === "last_updated_state" | ||
) { | ||
return { | ||
entity: entityId, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import { html, LitElement } from "lit"; | ||
import { customElement, property } from "lit/decorators"; | ||
import memoizeOne from "memoize-one"; | ||
import { | ||
assert, | ||
literal, | ||
number, | ||
object, | ||
optional, | ||
string, | ||
union, | ||
} from "superstruct"; | ||
import { fireEvent } from "../../../../../common/dom/fire_event"; | ||
import "../../../../../components/ha-form/ha-form"; | ||
import type { | ||
SchemaUnion, | ||
HaFormSchema, | ||
} from "../../../../../components/ha-form/types"; | ||
import { forDictStruct } from "../../../../config/automation/structs"; | ||
import type { HomeAssistant } from "../../../../../types"; | ||
import type { | ||
LastUpdatedStateCondition, | ||
StateCondition, | ||
} from "../../../common/validate-condition"; | ||
|
||
const lastUpdatedStateConditionStruct = object({ | ||
condition: literal("last_updated_state"), | ||
entity: optional(string()), | ||
within: optional(union([number(), string(), forDictStruct])), | ||
after: optional(union([number(), string(), forDictStruct])), | ||
}); | ||
|
||
@customElement("ha-card-condition-last_updated_state") | ||
export class HaCardConditionLastUpdatedState extends LitElement { | ||
@property({ attribute: false }) public hass!: HomeAssistant; | ||
|
||
@property({ attribute: false }) public condition!: LastUpdatedStateCondition; | ||
|
||
@property({ type: Boolean }) public disabled = false; | ||
|
||
public static get defaultConfig(): LastUpdatedStateCondition { | ||
return { condition: "last_updated_state", entity: "" }; | ||
} | ||
|
||
protected static validateUIConfig(condition: StateCondition) { | ||
return assert(condition, lastUpdatedStateConditionStruct); | ||
} | ||
|
||
private _schema = memoizeOne( | ||
() => | ||
[ | ||
{ name: "entity", selector: { entity: {} } }, | ||
{ | ||
name: "", | ||
type: "grid", | ||
schema: [ | ||
{ | ||
name: "within", | ||
selector: { | ||
duration: {}, | ||
}, | ||
}, | ||
{ | ||
name: "after", | ||
selector: { | ||
duration: {}, | ||
}, | ||
}, | ||
], | ||
}, | ||
] as const satisfies readonly HaFormSchema[] | ||
); | ||
|
||
protected render() { | ||
const stateObj = this.condition.entity | ||
? this.hass.states[this.condition.entity] | ||
: undefined; | ||
|
||
return html` | ||
<ha-form | ||
.hass=${this.hass} | ||
.data=${this.condition} | ||
.schema=${this._schema(stateObj)} | ||
.disabled=${this.disabled} | ||
@value-changed=${this._valueChanged} | ||
.computeLabel=${this._computeLabelCallback} | ||
></ha-form> | ||
`; | ||
} | ||
|
||
private _valueChanged(ev: CustomEvent): void { | ||
ev.stopPropagation(); | ||
const condition = ev.detail.value as LastUpdatedStateCondition; | ||
fireEvent(this, "value-changed", { value: condition }); | ||
} | ||
|
||
private _computeLabelCallback = ( | ||
schema: SchemaUnion<ReturnType<typeof this._schema>> | ||
): string => { | ||
switch (schema.name) { | ||
case "entity": | ||
return this.hass.localize("ui.components.entity.entity-picker.entity"); | ||
case "within": | ||
case "after": | ||
return this.hass.localize( | ||
"ui.panel.config.automation.editor.triggers.type.state.for" | ||
); | ||
default: | ||
return ""; | ||
} | ||
}; | ||
} | ||
|
||
declare global { | ||
interface HTMLElementTagNameMap { | ||
"ha-card-condition-last_updated_state": HaCardConditionLastUpdatedState; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -7136,6 +7136,9 @@ | |||||
"above": "Above", | ||||||
"below": "Below" | ||||||
}, | ||||||
"last_updated_state": { | ||||||
"label": "Entity updated" | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I understand this is the name of the condition. In that case I'd use "Last entity update" here instead as this popup menu contains only nouns so far.
Suggested change
|
||||||
}, | ||||||
"screen": { | ||||||
"label": "Screen", | ||||||
"breakpoints": "Screen sizes", | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are mixing last updated and last changed, these are 2 different things.
If an entity is updated, it doesn't necessarily have to have changed the state, it could still be in the same state as before the update. It just fetched updated data.