More Info: Add History Tab (#6758)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Zack Arnett 2020-09-03 18:28:10 -05:00 committed by GitHub
parent be8812e0af
commit 1431e75f8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 428 additions and 183 deletions

View File

@ -23,7 +23,8 @@ export const getLogbookData = (
hass: HomeAssistant, hass: HomeAssistant,
startDate: string, startDate: string,
endDate: string, endDate: string,
entityId?: string entityId?: string,
entity_matches_only?: boolean
) => { ) => {
const ALL_ENTITIES = "*"; const ALL_ENTITIES = "*";
@ -51,7 +52,8 @@ export const getLogbookData = (
hass, hass,
startDate, startDate,
endDate, endDate,
entityId !== ALL_ENTITIES ? entityId : undefined entityId !== ALL_ENTITIES ? entityId : undefined,
entity_matches_only
).then((entries) => entries.reverse()); ).then((entries) => entries.reverse());
return DATA_CACHE[cacheKey][entityId]; return DATA_CACHE[cacheKey][entityId];
}; };
@ -60,11 +62,13 @@ const getLogbookDataFromServer = async (
hass: HomeAssistant, hass: HomeAssistant,
startDate: string, startDate: string,
endDate: string, endDate: string,
entityId?: string entityId?: string,
entity_matches_only?: boolean
) => { ) => {
const url = `logbook/${startDate}?end_time=${endDate}${ const url = `logbook/${startDate}?end_time=${endDate}${
entityId ? `&entity=${entityId}` : "" entityId ? `&entity=${entityId}` : ""
}`; }${entity_matches_only ? `&entity_matches_only` : ""}`;
return hass.callApi<LogbookEntry[]>("GET", url); return hass.callApi<LogbookEntry[]>("GET", url);
}; };

View File

@ -1,33 +1,34 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@material/mwc-icon-button"; import "@material/mwc-icon-button";
import "../../components/ha-header-bar"; import "@material/mwc-tab";
import "../../components/ha-dialog"; import "@material/mwc-tab-bar";
import "../../components/ha-svg-icon"; import { mdiClose, mdiCog, mdiPencil } from "@mdi/js";
import {
css,
customElement,
html,
internalProperty,
LitElement,
property,
} from "lit-element";
import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const"; import { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import { navigate } from "../../common/navigate"; import { navigate } from "../../common/navigate";
import { fireEvent } from "../../common/dom/fire_event"; import "../../components/ha-dialog";
import "../../components/ha-header-bar";
import "../../components/ha-svg-icon";
import "../../components/state-history-charts"; import "../../components/state-history-charts";
import { removeEntityRegistryEntry } from "../../data/entity_registry"; import { removeEntityRegistryEntry } from "../../data/entity_registry";
import { showEntityEditorDialog } from "../../panels/config/entities/show-dialog-entity-editor"; import { showEntityEditorDialog } from "../../panels/config/entities/show-dialog-entity-editor";
import "../../panels/logbook/ha-logbook";
import { haStyleDialog } from "../../resources/styles";
import "../../state-summary/state-card-content"; import "../../state-summary/state-card-content";
import { HomeAssistant } from "../../types";
import { showConfirmationDialog } from "../generic/show-dialog-box"; import { showConfirmationDialog } from "../generic/show-dialog-box";
import "./more-info-content"; import "./more-info-content";
import {
customElement,
LitElement,
property,
internalProperty,
css,
html,
} from "lit-element";
import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { getRecentWithCache } from "../../data/cached-history";
import { computeDomain } from "../../common/entity/compute_domain";
import { mdiClose, mdiCog, mdiPencil } from "@mdi/js";
import { HistoryResult } from "../../data/history";
const DOMAINS_NO_INFO = ["camera", "configurator"]; const DOMAINS_NO_INFO = ["camera", "configurator"];
const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"]; const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"];
@ -43,11 +44,9 @@ export class MoreInfoDialog extends LitElement {
@property({ type: Boolean, reflect: true }) public large = false; @property({ type: Boolean, reflect: true }) public large = false;
@internalProperty() private _stateHistory?: HistoryResult;
@internalProperty() private _entityId?: string | null; @internalProperty() private _entityId?: string | null;
private _historyRefreshInterval?: number; @internalProperty() private _currTabIndex = 0;
public showDialog(params: MoreInfoDialogParams) { public showDialog(params: MoreInfoDialogParams) {
this._entityId = params.entityId; this._entityId = params.entityId;
@ -55,21 +54,11 @@ export class MoreInfoDialog extends LitElement {
this.closeDialog(); this.closeDialog();
} }
this.large = false; this.large = false;
this._stateHistory = undefined;
if (this._computeShowHistoryComponent(this._entityId)) {
this._getStateHistory();
clearInterval(this._historyRefreshInterval);
this._historyRefreshInterval = window.setInterval(() => {
this._getStateHistory();
}, 60 * 1000);
}
} }
public closeDialog() { public closeDialog() {
this._entityId = undefined; this._entityId = undefined;
this._stateHistory = undefined; this._currTabIndex = 0;
clearInterval(this._historyRefreshInterval);
this._historyRefreshInterval = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
@ -93,11 +82,14 @@ export class MoreInfoDialog extends LitElement {
hideActions hideActions
data-domain=${domain} data-domain=${domain}
> >
<ha-header-bar slot="heading"> <div slot="heading" class="heading">
<ha-header-bar>
<mwc-icon-button <mwc-icon-button
slot="navigationIcon" slot="navigationIcon"
.label=${this.hass.localize("ui.dialogs.more_info_control.dismiss")}
dialogAction="cancel" dialogAction="cancel"
.label=${this.hass.localize(
"ui.dialogs.more_info_control.dismiss"
)}
> >
<ha-svg-icon .path=${mdiClose}></ha-svg-icon> <ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button> </mwc-icon-button>
@ -105,7 +97,8 @@ export class MoreInfoDialog extends LitElement {
${computeStateName(stateObj)} ${computeStateName(stateObj)}
</div> </div>
${this.hass.user!.is_admin ${this.hass.user!.is_admin
? html`<mwc-icon-button ? html`
<mwc-icon-button
slot="actionItems" slot="actionItems"
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.settings" "ui.dialogs.more_info_control.settings"
@ -113,13 +106,15 @@ export class MoreInfoDialog extends LitElement {
@click=${this._gotoSettings} @click=${this._gotoSettings}
> >
<ha-svg-icon .path=${mdiCog}></ha-svg-icon> <ha-svg-icon .path=${mdiCog}></ha-svg-icon>
</mwc-icon-button>` </mwc-icon-button>
`
: ""} : ""}
${this.hass.user!.is_admin && ${this.hass.user!.is_admin &&
((EDITABLE_DOMAINS_WITH_ID.includes(domain) && ((EDITABLE_DOMAINS_WITH_ID.includes(domain) &&
stateObj.attributes.id) || stateObj.attributes.id) ||
EDITABLE_DOMAINS.includes(domain)) EDITABLE_DOMAINS.includes(domain))
? html` <mwc-icon-button ? html`
<mwc-icon-button
slot="actionItems" slot="actionItems"
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.edit" "ui.dialogs.more_info_control.edit"
@ -127,36 +122,49 @@ export class MoreInfoDialog extends LitElement {
@click=${this._gotoEdit} @click=${this._gotoEdit}
> >
<ha-svg-icon .path=${mdiPencil}></ha-svg-icon> <ha-svg-icon .path=${mdiPencil}></ha-svg-icon>
</mwc-icon-button>` </mwc-icon-button>
`
: ""} : ""}
</ha-header-bar> </ha-header-bar>
${this._computeShowHistoryComponent(entityId)
? html`
<mwc-tab-bar
.activeIndex=${this._currTabIndex}
@MDCTabBar:activated=${this._handleTabChanged}
>
<mwc-tab
.label=${this.hass.localize(
"ui.dialogs.more_info_control.controls"
)}
></mwc-tab>
<mwc-tab
.label=${this.hass.localize(
"ui.dialogs.more_info_control.history"
)}
></mwc-tab>
</mwc-tab-bar>
`
: ""}
</div>
<div class="content"> <div class="content">
${this._currTabIndex === 0
? html`
${DOMAINS_NO_INFO.includes(domain) ${DOMAINS_NO_INFO.includes(domain)
? "" ? ""
: html` : html`
<state-card-content <state-card-content
in-dialog
.stateObj=${stateObj} .stateObj=${stateObj}
.hass=${this.hass} .hass=${this.hass}
in-dialog
></state-card-content> ></state-card-content>
`} `}
${this._computeShowHistoryComponent(entityId)
? html`
<state-history-charts
.hass=${this.hass}
.historyData=${this._stateHistory}
up-to-now
.isLoadingData=${!this._stateHistory}
></state-history-charts>
`
: ""}
<more-info-content <more-info-content
.stateObj=${stateObj} .stateObj=${stateObj}
.hass=${this.hass} .hass=${this.hass}
></more-info-content> ></more-info-content>
${stateObj.attributes.restored ${stateObj.attributes.restored
? html`<p> ? html`
<p>
${this.hass.localize( ${this.hass.localize(
"ui.dialogs.more_info_control.restored.not_provided" "ui.dialogs.more_info_control.restored.not_provided"
)} )}
@ -170,32 +178,27 @@ export class MoreInfoDialog extends LitElement {
${this.hass.localize( ${this.hass.localize(
"ui.dialogs.more_info_control.restored.remove_action" "ui.dialogs.more_info_control.restored.remove_action"
)} )}
</mwc-button>` </mwc-button>
`
: ""} : ""}
`
: html`
<ha-more-info-tab-history
.hass=${this.hass}
.entityId=${this._entityId}
></ha-more-info-tab-history>
`}
</div> </div>
</ha-dialog> </ha-dialog>
`; `;
} }
private _enlarge() { protected firstUpdated(): void {
this.large = !this.large; import("./ha-more-info-tab-history");
} }
private async _getStateHistory(): Promise<void> { private _enlarge() {
if (!this._entityId) { this.large = !this.large;
return;
}
this._stateHistory = await getRecentWithCache(
this.hass!,
this._entityId,
{
refresh: 60,
cacheKey: `more_info.${this._entityId}`,
hoursToShow: 24,
},
this.hass!.localize,
this.hass!.language
);
} }
private _computeShowHistoryComponent(entityId) { private _computeShowHistoryComponent(entityId) {
@ -243,6 +246,15 @@ export class MoreInfoDialog extends LitElement {
this.closeDialog(); this.closeDialog();
} }
private _handleTabChanged(ev: CustomEvent): void {
const newTab = ev.detail.index;
if (newTab === this._currTabIndex) {
return;
}
this._currTabIndex = ev.detail.index;
}
static get styles() { static get styles() {
return [ return [
haStyleDialog, haStyleDialog,
@ -256,8 +268,6 @@ export class MoreInfoDialog extends LitElement {
--mdc-theme-on-primary: var(--primary-text-color); --mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface); --mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0; flex-shrink: 0;
border-bottom: 1px solid
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
} }
@media all and (max-width: 450px), all and (max-height: 500px) { @media all and (max-width: 450px), all and (max-height: 500px) {
@ -268,6 +278,11 @@ export class MoreInfoDialog extends LitElement {
} }
} }
.heading {
border-bottom: 1px solid
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
}
@media all and (min-width: 451px) and (min-height: 501px) { @media all and (min-width: 451px) and (min-height: 501px) {
ha-dialog { ha-dialog {
--mdc-dialog-max-width: 90vw; --mdc-dialog-max-width: 90vw;
@ -306,8 +321,7 @@ export class MoreInfoDialog extends LitElement {
--dialog-content-padding: 0; --dialog-content-padding: 0;
} }
state-card-content, state-card-content {
state-history-charts {
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;
} }
@ -315,3 +329,9 @@ export class MoreInfoDialog extends LitElement {
]; ];
} }
} }
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-dialog": MoreInfoDialog;
}
}

View File

@ -0,0 +1,164 @@
import {
css,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import "../../components/ha-circular-progress";
import "../../components/state-history-charts";
import { getRecentWithCache } from "../../data/cached-history";
import { HistoryResult } from "../../data/history";
import { getLogbookData, LogbookEntry } from "../../data/logbook";
import "../../panels/logbook/ha-logbook";
import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
@customElement("ha-more-info-tab-history")
export class MoreInfoTabHistoryDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entityId!: string;
@internalProperty() private _stateHistory?: HistoryResult;
@internalProperty() private _entries?: LogbookEntry[];
@internalProperty() private _persons = {};
private _historyRefreshInterval?: number;
protected render(): TemplateResult {
if (!this.entityId) {
return html``;
}
const stateObj = this.hass.states[this.entityId];
if (!stateObj) {
return html``;
}
return html`
<state-history-charts
up-to-now
.hass=${this.hass}
.historyData=${this._stateHistory}
.isLoadingData=${!this._stateHistory}
></state-history-charts>
${!this._entries
? html`
<ha-circular-progress
active
alt=${this.hass.localize("ui.common.loading")}
></ha-circular-progress>
`
: html`
<ha-logbook
narrow
no-icon
no-name
class=${classMap({
"has-entries": Boolean(this._entries?.length),
})}
.hass=${this.hass}
.entries=${this._entries}
.userIdToName=${this._persons}
></ha-logbook>
`}
`;
}
protected firstUpdated(): void {
this._fetchPersonNames();
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this.entityId) {
clearInterval(this._historyRefreshInterval);
}
if (changedProps.has("entityId")) {
this._stateHistory = undefined;
this._entries = undefined;
this._getStateHistory();
this._getLogBookData();
clearInterval(this._historyRefreshInterval);
this._historyRefreshInterval = window.setInterval(() => {
this._getStateHistory();
}, 60 * 1000);
}
}
private async _getStateHistory(): Promise<void> {
this._stateHistory = await getRecentWithCache(
this.hass!,
this.entityId,
{
refresh: 60,
cacheKey: `more_info.${this.entityId}`,
hoursToShow: 24,
},
this.hass!.localize,
this.hass!.language
);
}
private async _getLogBookData() {
const yesterday = new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
const now = new Date();
this._entries = await getLogbookData(
this.hass,
yesterday.toISOString(),
now.toISOString(),
this.entityId,
true
);
}
private _fetchPersonNames() {
Object.values(this.hass.states).forEach((entity) => {
if (
entity.attributes.user_id &&
computeStateDomain(entity) === "person"
) {
this._persons[entity.attributes.user_id] =
entity.attributes.friendly_name;
}
});
}
static get styles() {
return [
haStyleDialog,
css`
state-history-charts {
display: block;
margin-bottom: 16px;
}
ha-logbook.has-entries {
height: 360px;
}
ha-circular-progress {
display: flex;
justify-content: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-tab-history": MoreInfoTabHistoryDialog;
}
}

View File

@ -1,35 +1,48 @@
import { import {
css, css,
CSSResult, CSSResult,
customElement,
eventOptions,
html, html,
LitElement, LitElement,
property, property,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
eventOptions,
} from "lit-element"; } from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { scroll } from "lit-virtualizer"; import { scroll } from "lit-virtualizer";
import { formatDate } from "../../common/datetime/format_date"; import { formatDate } from "../../common/datetime/format_date";
import { formatTimeWithSeconds } from "../../common/datetime/format_time"; import { formatTimeWithSeconds } from "../../common/datetime/format_time";
import { restoreScroll } from "../../common/decorators/restore-scroll";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { domainIcon } from "../../common/entity/domain_icon"; import { domainIcon } from "../../common/entity/domain_icon";
import { stateIcon } from "../../common/entity/state_icon"; import { stateIcon } from "../../common/entity/state_icon";
import { computeRTL, emitRTLDirection } from "../../common/util/compute_rtl"; import { computeRTL, emitRTLDirection } from "../../common/util/compute_rtl";
import "../../components/ha-circular-progress";
import "../../components/ha-icon"; import "../../components/ha-icon";
import { LogbookEntry } from "../../data/logbook"; import { LogbookEntry } from "../../data/logbook";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { restoreScroll } from "../../common/decorators/restore-scroll";
@customElement("ha-logbook")
class HaLogbook extends LitElement { class HaLogbook extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public userIdToName = {}; @property({ attribute: false }) public userIdToName = {};
@property() public entries: LogbookEntry[] = []; @property({ attribute: false }) public entries: LogbookEntry[] = [];
@property({ attribute: "rtl", type: Boolean, reflect: true }) @property({ type: Boolean, attribute: "narrow" })
public narrow = false;
@property({ attribute: "rtl", type: Boolean })
private _rtl = false; private _rtl = false;
@property({ type: Boolean, attribute: "no-icon" })
public noIcon = false;
@property({ type: Boolean, attribute: "no-name" })
public noName = false;
// @ts-ignore // @ts-ignore
@restoreScroll(".container") private _savedScrollPos?: number; @restoreScroll(".container") private _savedScrollPos?: number;
@ -52,14 +65,22 @@ class HaLogbook extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.entries?.length) { if (!this.entries?.length) {
return html` return html`
<div class="container" .dir=${emitRTLDirection(this._rtl)}> <div class="container no-entries" .dir=${emitRTLDirection(this._rtl)}>
${this.hass.localize("ui.panel.logbook.entries_not_found")} ${this.hass.localize("ui.panel.logbook.entries_not_found")}
</div> </div>
`; `;
} }
return html` return html`
<div class="container" @scroll=${this._saveScrollPos}> <div
class="container ${classMap({
narrow: this.narrow,
rtl: this._rtl,
"no-name": this.noName,
"no-icon": this.noIcon,
})}"
@scroll=${this._saveScrollPos}
>
${scroll({ ${scroll({
items: this.entries, items: this.entries,
renderItem: (item: LogbookEntry, index?: number) => renderItem: (item: LogbookEntry, index?: number) =>
@ -76,6 +97,7 @@ class HaLogbook extends LitElement {
if (index === undefined) { if (index === undefined) {
return html``; return html``;
} }
const previous = this.entries[index - 1]; const previous = this.entries[index - 1];
const state = item.entity_id ? this.hass.states[item.entity_id] : undefined; const state = item.entity_id ? this.hass.states[item.entity_id] : undefined;
const item_username = const item_username =
@ -98,12 +120,18 @@ class HaLogbook extends LitElement {
<div class="time"> <div class="time">
${formatTimeWithSeconds(new Date(item.when), this.hass.language)} ${formatTimeWithSeconds(new Date(item.when), this.hass.language)}
</div> </div>
<div class="icon-message">
${!this.noIcon
? html`
<ha-icon <ha-icon
.icon=${state ? stateIcon(state) : domainIcon(item.domain)} .icon=${state ? stateIcon(state) : domainIcon(item.domain)}
></ha-icon> ></ha-icon>
`
: ""}
<div class="message"> <div class="message">
${!item.entity_id ${!this.noName
? html` <span class="name">${item.name}</span> ` ? !item.entity_id
? html`<span class="name">${item.name}</span>`
: html` : html`
<a <a
href="#" href="#"
@ -112,17 +140,16 @@ class HaLogbook extends LitElement {
class="name" class="name"
>${item.name}</a >${item.name}</a
> >
`} `
<span : ""}
>${item.message}${item_username <span class="item-message">${item.message}</span>
? ` (${item_username})` <span>${item_username ? ` (${item_username})` : ``}</span>
: ``}</span
>
${!item.context_event_type ${!item.context_event_type
? "" ? ""
: item.context_event_type === "call_service" : item.context_event_type === "call_service"
? // Service Call ? // Service Call
html` by service ${item.context_domain}.${item.context_service}` html` by service
${item.context_domain}.${item.context_service}`
: item.context_entity_id === item.entity_id : item.context_entity_id === item.entity_id
? // HomeKit or something that self references ? // HomeKit or something that self references
html` by html` by
@ -141,6 +168,7 @@ class HaLogbook extends LitElement {
</div> </div>
</div> </div>
</div> </div>
</div>
`; `;
} }
@ -163,26 +191,36 @@ class HaLogbook extends LitElement {
height: 100%; height: 100%;
} }
:host([rtl]) { .rtl {
direction: ltr; direction: ltr;
} }
.entry { .entry {
display: flex; display: flex;
line-height: 2em; line-height: 2em;
padding-bottom: 8px;
} }
.time { .time {
width: 65px; width: 65px;
flex-shrink: 0; flex-shrink: 0;
font-size: 0.8em; font-size: 12px;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
:host([rtl]) .date { .rtl .date {
direction: rtl; direction: rtl;
} }
.icon-message {
display: flex;
align-items: center;
}
.no-entries {
text-align: center;
}
ha-icon { ha-icon {
margin: 0 8px 0 16px; margin: 0 8px 0 16px;
flex-shrink: 0; flex-shrink: 0;
@ -193,6 +231,10 @@ class HaLogbook extends LitElement {
color: var(--primary-text-color); color: var(--primary-text-color);
} }
.no-name .item-message {
text-transform: capitalize;
}
a { a {
color: var(--primary-color); color: var(--primary-color);
} }
@ -212,8 +254,21 @@ class HaLogbook extends LitElement {
.uni-virtualizer-host > * { .uni-virtualizer-host > * {
box-sizing: border-box; box-sizing: border-box;
} }
.narrow .entry {
flex-direction: column-reverse;
line-height: 1.5;
}
.narrow .icon-message ha-icon {
margin-left: 0;
}
`; `;
} }
} }
customElements.define("ha-logbook", HaLogbook); declare global {
interface HTMLElementTagNameMap {
"ha-logbook": HaLogbook;
}
}

View File

@ -389,6 +389,8 @@
"dismiss": "Dismiss dialog", "dismiss": "Dismiss dialog",
"settings": "Entity settings", "settings": "Entity settings",
"edit": "Edit entity", "edit": "Edit entity",
"controls": "Controls",
"history": "History",
"script": { "script": {
"last_action": "Last Action", "last_action": "Last Action",
"last_triggered": "Last Triggered" "last_triggered": "Last Triggered"