3){let i=t.slice(3);if(t.includes("filter")){const t=i.findIndex(e=>"filter"==e),s=i.slice(t+1);i=i.slice(0,t),a=Object.assign(Object.assign({},a),{filter:e(s)})}i.length&&(i.length%2&&(a=Object.assign(Object.assign({},a),{subpage:i.shift()})),i.length&&(a=Object.assign(Object.assign({},a),{params:e(i)})))}return a},Ui=(e,...t)=>{let a={page:e,params:{}};t.forEach(e=>{"string"==typeof e?a=Object.assign(Object.assign({},a),{subpage:e}):"params"in e?a=Object.assign(Object.assign({},a),{params:e.params}):"filter"in e&&(a=Object.assign(Object.assign({},a),{filter:e.filter}))});const i=e=>{let t=Object.keys(e);t=t.filter(t=>e[t]),t.sort();let a="";return t.forEach(t=>{let i=e[t];a=a.length?`${a}/${t}/${i}`:`${t}/${i}`}),a};let s="/alarmo/"+a.page;return a.subpage&&(s=`${s}/${a.subpage}`),i(a.params).length&&(s=`${s}/${i(a.params)}`),a.filter&&(s=`${s}/filter/${i(a.filter)}`),s};let Gi=class extends ne{constructor(){super(...arguments),this.min=0,this.max=100,this.step=10,this.value=0,this.scaleFactor=1,this.unit="",this.disabled=!1}firstUpdated(){this.value>0&&this.value<60&&(this.unit="sec"),"min"==this.unit&&(this.scaleFactor=1/60),"min"==this.unit&&(this.step=1)}render(){return q`
${this.getSlider()}
${this.getValue()}
`}getValue(){const e=Number(Math.round(this.value*this.scaleFactor));return!e&&this.zeroValue?this.zeroValue:`${e} ${this.getUnit()}`}getUnit(){switch(this.unit){case"sec":return gi("components.time_slider.seconds",this.hass.language);case"min":return gi("components.time_slider.minutes",this.hass.language);default:return""}}getSlider(){return q`
`}updateValue(e){const t=Number(e.target.value);this.value=Math.round(t/this.scaleFactor)}toggleUnit(){this.unit="min"==this.unit?"sec":"min",this.scaleFactor="min"==this.unit?1/60:1,this.step="min"==this.unit?1:10}};Gi.styles=o`
:host {
display: flex;
flex-direction: column;
min-width: 250px;
}
div.container {
display: grid;
grid-template-columns: max-content 1fr 60px;
grid-template-rows: min-content;
grid-template-areas: 'prefix slider value';
}
div.prefix {
grid-area: prefix;
display: flex;
align-items: center;
}
div.slider {
grid-area: slider;
display: flex;
align-items: center;
flex: 1;
}
div.value {
grid-area: value;
min-width: 40px;
display: flex;
align-items: center;
justify-content: flex-end;
cursor: pointer;
}
ha-slider {
width: 100%;
}
.disabled {
color: var(--disabled-text-color);
}
`,t([le({type:Number})],Gi.prototype,"min",void 0),t([le({type:Number})],Gi.prototype,"max",void 0),t([le({type:Number})],Gi.prototype,"step",void 0),t([le({type:Number})],Gi.prototype,"value",void 0),t([le()],Gi.prototype,"scaleFactor",void 0),t([le({type:String})],Gi.prototype,"unit",void 0),t([le({type:Boolean})],Gi.prototype,"disabled",void 0),t([le({type:String})],Gi.prototype,"zeroValue",void 0),Gi=t([re("time-slider")],Gi);var Fi="M8.59,16.58L13.17,12L8.59,7.41L10,6L16,12L10,18L8.59,16.58Z",Vi="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z";
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const Hi=2,Yi=6,Bi=e=>(...t)=>({_$litDirective$:e,values:t});class Ki{constructor(e){}T(e,t,a){this.Σdt=e,this.M=t,this.Σct=a}S(e,t){return this.update(e,t)}update(e,t){return this.render(...t)}}let Qi=class extends ne{constructor(){super(...arguments),this.label="",this.items=[],this.clearable=!1,this.icons=!1,this.disabled=!1,this.invalid=!1,this.rowRenderer=e=>{const t=Ci(e.description);return this.icons?q`
${e.name}
${t?q`
${e.description}
`:""}
`:q`
${e.name}
${t?q`
${e.description}
`:""}
`}}open(){this.updateComplete.then(()=>{var e,t;null===(t=null===(e=this.shadowRoot)||void 0===e?void 0:e.querySelector("vaadin-combo-box-light"))||void 0===t||t.open()})}disconnectedCallback(){super.disconnectedCallback(),this._overlayMutationObserver&&(this._overlayMutationObserver.disconnect(),this._overlayMutationObserver=void 0)}focus(){this.updateComplete.then(()=>{var e;(null===(e=this.shadowRoot)||void 0===e?void 0:e.querySelector("ha-textfield")).focus()})}shouldUpdate(e){if(e.get("items"))if(Mi(this.items,e.get("items"))){if(1==e.size)return!1}else this.firstUpdated();return!0}firstUpdated(){this._comboBox.items=this.items}render(){const e=Ci(this._value)&&this.items.find(e=>e.value==this._value);return q`
${this.clearable&&e?q`
`:""}
`}_clearValue(e){e.stopPropagation(),this._setValue("")}get _value(){return Ci(this.value)?this.value:""}_toggleOpen(e){var t,a,i,s,n,r;this.items.length?this._opened?(null===(i=null===(a=null===(t=this.shadowRoot)||void 0===t?void 0:t.querySelector("vaadin-combo-box-light"))||void 0===a?void 0:a.inputElement)||void 0===i||i.blur(),e.stopPropagation()):null===(r=null===(n=null===(s=this.shadowRoot)||void 0===s?void 0:s.querySelector("vaadin-combo-box-light"))||void 0===n?void 0:n.inputElement)||void 0===r||r.focus():e.stopPropagation()}_openedChanged(e){if(this._opened=e.detail.value,this._opened&&"MutationObserver"in window&&!this._overlayMutationObserver){const e=document.querySelector("vaadin-combo-box-overlay");if(!e)return;this._overlayMutationObserver=new MutationObserver(t=>{t.forEach(t=>{var a;"attributes"===t.type&&"inert"===t.attributeName&&!0===e.inert?(e.inert=!1,null===(a=this._overlayMutationObserver)||void 0===a||a.disconnect(),this._overlayMutationObserver=void 0):"childList"===t.type&&t.removedNodes.forEach(e=>{var t;"VAADIN-COMBO-BOX-OVERLAY"===e.nodeName&&(null===(t=this._overlayMutationObserver)||void 0===t||t.disconnect(),this._overlayMutationObserver=void 0)})})}),this._overlayMutationObserver.observe(e,{attributes:!0}),this._overlayMutationObserver.observe(document.body,{childList:!0})}}_valueChanged(e){const t=e.detail.value;t!==this._value&&this._setValue(t)}_setValue(e){this.value=e,setTimeout(()=>{De(this,"value-changed",{value:e})},0)}static get styles(){return o`
:host {
display: block;
}
vaadin-combo-box-light {
position: relative;
}
ha-textfield {
width: 100%;
}
ha-textfield > ha-icon-button {
--mdc-icon-button-size: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
ha-svg-icon {
color: var(--input-dropdown-icon-color);
position: absolute;
cursor: pointer;
}
ha-svg-icon.disabled {
cursor: default;
color: var(--disabled-text-color);
}
.toggle-button {
right: 12px;
bottom: 5px;
}
:host([opened]) .toggle-button {
color: var(--primary-color);
}
.clear-button {
--mdc-icon-size: 20px;
bottom: 5px;
right: 36px;
}
`}};t([le()],Qi.prototype,"label",void 0),t([le()],Qi.prototype,"value",void 0),t([le()],Qi.prototype,"items",void 0),t([le()],Qi.prototype,"clearable",void 0),t([le()],Qi.prototype,"icons",void 0),t([le({type:Boolean})],Qi.prototype,"disabled",void 0),t([de()],Qi.prototype,"_opened",void 0),t([le({attribute:"allow-custom-value",type:Boolean})],Qi.prototype,"allowCustomValue",void 0),t([le({type:Boolean})],Qi.prototype,"invalid",void 0),t([ce("vaadin-combo-box-light",!0)],Qi.prototype,"_comboBox",void 0),Qi=t([re("alarmo-select")],Qi);const Wi={};class Xi extends Ki{constructor(e){if(super(e),this.previousValue=Wi,e.type!==Yi)throw new Error("renderer only supports binding to element")}render(e,t){return I}update(e,[t,a]){var i;const s=this.previousValue===Wi;if(!this.hasChanged(a))return I;this.previousValue=Array.isArray(a)?Array.from(a):a;const n=e.element;if(s){const a=null===(i=e.options)||void 0===i?void 0:i.host;this.addRenderer(n,t,{host:a})}else this.runRenderer(n);return I}hasChanged(e){let t=!0;return Array.isArray(e)?Array.isArray(this.previousValue)&&this.previousValue.length===e.length&&e.every((e,t)=>e===this.previousValue[t])&&(t=!1):this.previousValue===e&&(t=!1),t}}const Zi=Bi(class extends Xi{addRenderer(e,t,a){e.renderer=(e,i,s)=>{G(t.call(a.host,s.item,s,i),e,a)}}runRenderer(e){e.requestContentUpdate()}}),Ji=(e,t)=>Zi(e,t);let es=class extends ne{static get styles(){return o`
:host {
display: block;
}
`}render(){return q`
`}constructor(){super(),this.addEventListener("clickHeader",this.manageSpoilers)}manageSpoilers(e){const t=e.target;t.getAttribute("active")?t.removeAttribute("active"):t.setAttribute("active","true"),this.querySelectorAll("alarmo-collapsible-header[active]").forEach((function(e){e!==t&&e.removeAttribute("active")}))}};es=t([re("alarmo-collapsible-group")],es);let ts=class extends ne{static get styles(){return o`
:host {
display: block;
}
`}render(){return q`
`}};ts=t([re("alarmo-collapsible-item")],ts);let as=class extends ne{constructor(){super(),this.clickHeader=new CustomEvent("clickHeader",{detail:{message:"clickHeader happened."},bubbles:!0,composed:!0}),this.active=!1,this.addEventListener("click",this.handleClick)}handleClick(){this.dispatchEvent(this.clickHeader)}render(){return q`
`}static get styles(){return o`
:host {
display: block;
cursor: pointer;
}
:host mwc-list-item::before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
content: '';
transition: opacity 15ms linear;
will-change: opacity;
background-color: black;
opacity: 0;
}
:host mwc-list-item:hover::before {
opacity: 0.04;
}
:host([active]) mwc-list-item::before {
opacity: 0.1;
}
:host([active]) mwc-list-item:hover::before {
opacity: 0.12;
}
:host mwc-list-item:active::before,
:host([active]) mwc-list-item:active::before {
opacity: 0.14;
}
::slotted(ha-icon) {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
}
:host mwc-list-item {
font-size: 15px;
--mdc-typography-body2-font-size: 14px;
}
:host .chevron {
display: block;
transition: 0.4s;
}
:host([active]) .chevron {
transform: rotate(180deg);
}
`}attributeChangedCallback(e,t,a){this.hasAttribute("active")&&this.nextElementSibling?this.nextElementSibling.style.maxHeight=this.nextElementSibling.scrollHeight+"px":this.nextElementSibling&&(this.nextElementSibling.style.maxHeight="0px"),super.attributeChangedCallback(e,t,a)}};t([le({type:CustomEvent})],as.prototype,"clickHeader",void 0),t([le({type:Boolean,attribute:!0,reflect:!0})],as.prototype,"active",void 0),as=t([re("alarmo-collapsible-header")],as);let is=class extends ne{static get styles(){return o`
:host {
display: block;
background-color: var(--card-background-color);
max-height: 0px;
overflow: hidden;
transition: max-height 0.2s ease-out;
}
.wrapper {
}
`}render(){return q`
Default details
`}};is=t([re("alarmo-collapsible-body")],is);let ss=class extends(et(ne)){hassSubscribe(){return this._fetchData(),[this.hass.connection.subscribeMessage(()=>this._fetchData(),{type:"alarmo_config_updated"})]}async _fetchData(){this.hass&&(this.areas=await Ze(this.hass),this.sensors=await Fe(this.hass))}async firstUpdated(){await this._fetchData(),this.selectedArea=Object.keys(this.areas)[0],this.data=Object.assign({},this.areas[this.selectedArea].modes)}render(){return this.data?q`
${gi("panels.general.cards.modes.description",this.hass.language)}
${Object.entries(ki).map(([e,t])=>{var a;return q`
${this.hass.localize("component.alarm_control_panel.state._."+t)}
${(null===(a=this.data[t])||void 0===a?void 0:a.enabled)?q`
${gi("common.enabled",this.hass.language)},
${gi("panels.general.cards.modes.number_sensors_active",this.hass.language,"number",this.getSensorsByMode(t))}
`:gi("common.disabled",this.hass.language)}
${this.renderModeConfig(t)}
`})}
`:q``}getSensorsByMode(e){return Object.values(this.sensors).filter(t=>t.modes.includes(e)||t.always_on).length}renderModeConfig(e){const t=e in this.data?this.data[e]:void 0;return q`
${gi("panels.general.cards.modes.modes."+e,this.hass.language)}
${gi("panels.general.cards.modes.fields.status.heading",this.hass.language)}
${gi("panels.general.cards.modes.fields.status.description",this.hass.language)}
this.saveData(e,{enabled:!0})}>
${gi("common.enabled",this.hass.language)}
this.saveData(e,{enabled:!1})}
>
${gi("common.disabled",this.hass.language)}
${gi("panels.general.cards.modes.fields.exit_delay.heading",this.hass.language)}
${gi("panels.general.cards.modes.fields.exit_delay.description",this.hass.language)}
this.saveData(e,{exit_time:Number(t.target.value)})}
?disabled=${!(null==t?void 0:t.enabled)}
>
${gi("panels.general.cards.modes.fields.entry_delay.heading",this.hass.language)}
${gi("panels.general.cards.modes.fields.entry_delay.description",this.hass.language)}
this.saveData(e,{entry_time:Number(t.target.value)})}
?disabled=${!(null==t?void 0:t.enabled)}
>
${gi("panels.general.cards.modes.fields.trigger_time.heading",this.hass.language)}
${gi("panels.general.cards.modes.fields.trigger_time.description",this.hass.language)}
this.saveData(e,{trigger_time:Number(t.target.value)})}
?disabled=${!(null==t?void 0:t.enabled)}
>
`}selectArea(e){e!=this.selectedArea&&(this.selectedArea=e,this.data=Object.assign({},this.areas[e].modes))}saveClick(e){Je(this.hass,{area_id:this.selectedArea,modes:this.data}).catch(t=>Di(t,e)).then()}saveData(e,t){this.data=Object.assign(Object.assign({},this.data),{[e]:Object.assign(Object.assign({},this.data[e]||{enabled:!1,exit_time:0,entry_time:0,trigger_time:0}),t)}),Je(this.hass,{area_id:this.selectedArea,modes:this.data}).catch(e=>Di(e,this.shadowRoot.querySelector("ha-card"))).then()}static get styles(){return o`
${qi}
alarmo-collapsible-header:first-of-type {
border-top: 1px solid var(--divider-color);
}
.description {
margin: 8px;
padding: 12px;
color: var(--primary-color);
filter: brightness(0.85);
font-size: 14px;
line-height: 1.5em;
min-height: 36px;
display: flex;
align-items: center;
position: relative;
}
.description::before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
content: '';
background: rgba(var(--rgb-primary-color), 0.12);
border-radius: 5px;
}
.description ha-icon {
--mdc-icon-size: 36px;
display: inline;
float: left;
margin-right: 12px;
align-self: flex-start;
}
`}};t([le()],ss.prototype,"hass",void 0),t([le({type:Boolean})],ss.prototype,"narrow",void 0),t([le()],ss.prototype,"config",void 0),t([le()],ss.prototype,"areas",void 0),t([le()],ss.prototype,"sensors",void 0),t([le()],ss.prototype,"data",void 0),t([le()],ss.prototype,"selectedArea",void 0),ss=t([re("alarm-mode-card")],ss);let ns=class extends ne{constructor(){super(...arguments),this.threeLine=!1}render(){return q`
`}static get styles(){return o`
:host {
display: flex;
flex-direction: row;
padding: 0px 16px;
align-items: center;
min-height: 72px;
}
:host([large]) {
align-items: normal;
flex-direction: column;
border-top: 1px solid var(--divider-color);
border-bottom: 1px solid var(--divider-color);
padding: 16px 16px;
}
:host([narrow]) {
align-items: normal;
flex-direction: column;
border-bottom: none;
border-top: 1px solid var(--divider-color);
padding: 16px 16px;
}
:host([nested]) {
border: none;
padding: 0px 16px 16px 16px;
margin-top: -16px;
min-height: 40px;
}
:host([nested]:not([narrow])) {
padding: 16px 16px 16px 32px;
}
:host([first]) {
border-top: none;
}
:host([last]) {
border-bottom: none;
}
:host([dialog]) {
border: none;
padding: 12px 0px;
}
::slotted(ha-switch) {
padding: 16px 0;
}
.info {
flex: 1 0 60px;
}
:host([large]) .info,
:host([narrow]) .info {
flex: 1 0 40px;
}
:host([nested]) .info {
flex: 1 0 26px;
}
:host([dialog]) .info {
flex: 1 0 40px;
padding-bottom: 8px;
}
.secondary {
color: var(--secondary-text-color);
}
:host(:not([large]):not([narrow])):not([dialog])) ::slotted(*) {
max-width: 66%;
}
`}};t([le({type:Boolean,reflect:!0})],ns.prototype,"narrow",void 0),t([le({type:Boolean,reflect:!0})],ns.prototype,"large",void 0),t([le({type:Boolean,attribute:"three-line"})],ns.prototype,"threeLine",void 0),t([le({type:Boolean})],ns.prototype,"nested",void 0),t([le({type:Boolean})],ns.prototype,"dialog",void 0),ns=t([re("settings-row")],ns);let rs=class extends ne{constructor(){super(...arguments),this.header="",this.open=!1}render(){return q`
${this.open?q`
`:q`
`}
`}static get styles(){return o`
:host {
}
div.header {
display: flex;
align-items: center;
padding: 0px 16px;
cursor: pointer;
}
div.header.open:first-of-type {
border-bottom: 1px solid var(--divider-color);
}
div.header.open:last-of-type {
border-top: 1px solid var(--divider-color);
}
:host([narrow]) div.header {
border-top: 1px solid var(--divider-color);
border-bottom: none;
}
div.header span {
display: flex;
flex-grow: 1;
}
div.seperator {
height: 1px;
background: var(--divider-color);
}
`}};t([le({type:Boolean,reflect:!0})],rs.prototype,"narrow",void 0),t([le()],rs.prototype,"header",void 0),t([le()],rs.prototype,"open",void 0),rs=t([re("collapsible-section")],rs);let os=class extends(et(ne)){constructor(){super(...arguments),this.areas={}}hassSubscribe(){return this._fetchData(),[this.hass.connection.subscribeMessage(()=>this._fetchData(),{type:"alarmo_config_updated"})]}async _fetchData(){if(!this.hass)return;const e=await Ge(this.hass);this.config=e,this.areas=await Ze(this.hass),this.selection=e.mqtt}firstUpdated(){(async()=>{await Ie()})()}render(){return this.hass&&this.selection?q`
${gi("panels.general.cards.mqtt.description",this.hass.language)}
${gi("panels.general.cards.mqtt.fields.state_topic.heading",this.hass.language)}
${gi("panels.general.cards.mqtt.fields.state_topic.description",this.hass.language)}
{this.selection={...this.selection,state_topic:e.target.value}}}
>
${Object.values(fi).filter(e=>Object.values(this.areas).some(t=>Li(e,t.modes))).map(e=>q`
${xi(e)}
${gi("panels.general.cards.mqtt.fields.state_payload.item",this.hass.language,"{state}",xi(e))}
{this.selection=zi(this.selection,{state_payload:{[e]:t.target.value}})}}
>
`)}
${gi("panels.general.cards.mqtt.fields.event_topic.heading",this.hass.language)}
${gi("panels.general.cards.mqtt.fields.event_topic.description",this.hass.language)}
{this.selection={...this.selection,event_topic:e.target.value}}}
>
${gi("panels.general.cards.mqtt.fields.command_topic.heading",this.hass.language)}
${gi("panels.general.cards.mqtt.fields.command_topic.description",this.hass.language)}
{this.selection={...this.selection,command_topic:e.target.value}}}
>
${Object.values(_i).filter(e=>Object.values(this.areas).some(t=>Li((e=>{switch(e){case _i.COMMAND_ALARM_DISARM:return fi.STATE_ALARM_DISARMED;case _i.COMMAND_ALARM_ARM_HOME:return fi.STATE_ALARM_ARMED_HOME;case _i.COMMAND_ALARM_ARM_AWAY:return fi.STATE_ALARM_ARMED_AWAY;case _i.COMMAND_ALARM_ARM_NIGHT:return fi.STATE_ALARM_ARMED_NIGHT;case _i.COMMAND_ALARM_ARM_CUSTOM_BYPASS:return fi.STATE_ALARM_ARMED_CUSTOM_BYPASS;case _i.COMMAND_ALARM_ARM_VACATION:return fi.STATE_ALARM_ARMED_VACATION;default:return}})(e),t.modes))).map(e=>q`
${xi(e)}
${gi("panels.general.cards.mqtt.fields.command_payload.item",this.hass.language,"{command}",xi(e))}
{this.selection=zi(this.selection,{command_payload:{[e]:t.target.value}})}}
>
`)}
${gi("panels.general.cards.mqtt.fields.require_code.heading",this.hass.language)}
${gi("panels.general.cards.mqtt.fields.require_code.description",this.hass.language)}
{this.selection={...this.selection,require_code:e.target.checked}}}
>
${this.hass.localize("ui.common.save")}
`:q``}saveClick(e){this.hass&&Be(this.hass,{mqtt:Object.assign(Object.assign({},this.selection),{enabled:!0})}).catch(t=>Di(t,e)).then(()=>{this.cancelClick()})}cancelClick(){Pe(0,Ui("general"),!0)}};os.styles=qi,t([le()],os.prototype,"narrow",void 0),t([le()],os.prototype,"config",void 0),t([le()],os.prototype,"areas",void 0),t([le()],os.prototype,"selection",void 0),os=t([re("mqtt-config-card")],os);
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
class ls extends Ki{constructor(e){if(super(e),this.vt=I,e.type!==Hi)throw Error(this.constructor.directiveName+"() can only be used in child bindings")}render(e){if(e===I)return this.Vt=void 0,this.vt=e;if(e===R)return e;if("string"!=typeof e)throw Error(this.constructor.directiveName+"() called with a non-string value");if(e===this.vt)return this.Vt;this.vt=e;const t=[e];return t.raw=t,this.Vt={_$litType$:this.constructor.resultType,strings:t,values:[]}}}ls.directiveName="unsafeHTML",ls.resultType=1;const ds=Bi(ls);let cs=class extends ne{render(){return q`
${this.renderCheckmark()}
${this.renderButton()}
`}renderCheckmark(){return this.checkmark?q`
`:q``}renderButton(){return this.cancellable?q`
`:void 0!==this.badge?q`
${this.badge}
`:q``}_toggleSelect(){if(!this.value||!this.clickable)return;this.selectable&&(this.checked=!this.checked);const e=new CustomEvent("value-changed",{detail:this.value});this.dispatchEvent(e)}_buttonClick(){const e=new CustomEvent("button-clicked",{detail:this.value});this.dispatchEvent(e)}static get styles(){return o`
:host {
margin: 4px;
}
.chip {
display: flex;
position: relative;
height: 36px;
padding: 0px 16px;
align-items: center;
color: var(--primary-text-color);
user-select: none;
font-weight: 400;
z-index: 1;
}
:host([clickable]) .chip {
cursor: pointer;
color: var(--rgb-primary-color);
opacity: 0.85;
}
.chip:before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
content: '';
border-radius: 8px;
border: 1px solid var(--primary-text-color);
opacity: 0.24;
z-index: -1;
}
:host([clickable]) .chip:hover,
:host([clickable]) .chip:active {
opacity: 1;
}
:host([clickable]) .chip:hover:before {
opacity: 0.3;
}
:host([clickable]) .chip:active:before {
opacity: 0.06;
background: var(--primary-text-color);
}
:host .chip.selected:before,
:host([cancellable]) .chip:before {
background: rgba(var(--rgb-primary-color), 0.18);
border: none;
opacity: 1;
}
:host .chip.selected:hover:before {
background: rgba(var(--rgb-primary-color), 0.24);
opacity: 1;
}
:host .chip.selected:active:before {
background: rgba(var(--rgb-primary-color), 0.3);
opacity: 1;
}
.chip div.checkmark-container {
width: 0px;
height: 100%;
transition: width 0.1s ease-in-out;
overflow: hidden;
display: flex;
align-items: center;
margin: 0px 4px 0px -4px;
--mdc-icon-size: 18px;
}
.chip.selected div.checkmark-container {
width: 18px;
}
.chip div.button-container {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
margin: 0px -16px 0px 6px;
cursor: pointer;
--mdc-icon-size: 20px;
position: relative;
z-index: 1;
opacity: 0.85;
color: var(--dark-primary-color);
}
.chip div.button-container:before {
position: absolute;
top: 3px;
right: 3px;
bottom: 3px;
left: 3px;
pointer-events: none;
content: '';
border-radius: 15px;
z-index: -1;
}
.chip div.button-container:hover,
.chip div.button-container:hover {
opacity: 1;
}
.chip div.button-container:hover:before {
background: rgba(var(--rgb-primary-color), 0.12);
}
.chip div.button-container:active:before {
background: rgba(var(--rgb-primary-color), 0.24);
}
.chip div.badge-container {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
margin: 0px -9px 0px 9px;
position: relative;
z-index: 1;
font-size: 0.875em;
}
.chip div.badge-container:before {
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
pointer-events: none;
content: '';
border-radius: 50%;
z-index: -1;
}
:host([table]) .chip {
height: 40px;
}
:host([table]) .chip:before {
border-radius: 4px;
}
`}};t([le({type:String})],cs.prototype,"value",void 0),t([le({type:Boolean})],cs.prototype,"checked",void 0),t([le({type:Boolean})],cs.prototype,"checkmark",void 0),t([le({type:Boolean})],cs.prototype,"selectable",void 0),t([le({type:Boolean})],cs.prototype,"clickable",void 0),t([le({type:Boolean})],cs.prototype,"cancellable",void 0),t([le({type:Number})],cs.prototype,"badge",void 0),t([le({type:Boolean})],cs.prototype,"table",void 0),cs=t([re("alarmo-chip")],cs);let hs=class extends ne{constructor(){super(...arguments),this.value=[]}render(){return this.items?q`
${Object.values(this.items).map(e=>q`
${e.name}
`)}
`:q``}_itemChanged(e){const t=e.target.checked,a=String(e.detail);if(this.selectable){this.value.includes(a)&&!t?this.value=this.value.filter(e=>e!=a):!this.value.includes(a)&&t&&(this.value=[...this.value,a]);const e=new CustomEvent("value-changed",{detail:this.value});this.dispatchEvent(e)}else{const e=new CustomEvent("value-changed",{detail:a});this.dispatchEvent(e)}}static get styles(){return o`
:host {
display: flex;
flex-direction: row;
flex: 1;
margin: 0px -4px;
flex-wrap: wrap;
}
`}};t([le()],hs.prototype,"items",void 0),t([le()],hs.prototype,"value",void 0),t([le({type:Boolean})],hs.prototype,"selectable",void 0),hs=t([re("alarmo-chip-set")],hs);let us=class extends ne{set filters(e){this.filterConfig||(this.filterConfig=e)}shouldUpdate(e){return e.get("filters")&&!this.filterConfig&&(this.filterConfig=e.get("filters")),!0}render(){if(!this.columns||!this.data)return q``;const e=this.data.filter(e=>this.filterTableData(e,this.filterConfig));return q`
${this.renderFilterRow()}
${this.renderHeaderRow()}
${e.length?e.map(e=>this.renderDataRow(e)):q`
`}
`}renderHeaderRow(){return this.columns?q`
`:q``}renderDataRow(e){return this.columns?q`
this.handleClick(String(e.id))}
>
${Object.entries(this.columns).map(([t,a])=>a.hide?"":q`
${a.renderer?a.renderer(e):e[t]}
`)}
`:q``}filterTableData(e,t){return!t||Object.keys(t).every(a=>{if(!Object.keys(e).includes(a))return!0;const i=t[a].value;return!i||!i.length||(Array.isArray(e[a])?e[a].some(e=>i.includes(e)):i.includes(e[a]))})}_getFilteredItems(){return this.data.filter(e=>!this.filterTableData(e,this.filterConfig)).length}handleClick(e){if(!this.selectable)return;const t=new CustomEvent("row-click",{detail:{id:e}});this.dispatchEvent(t)}renderFilterRow(){var e;return this.filterConfig?q`
${this.renderFilterMenu()}
${this._getFilteredItems()?q`
${gi("components.table.filter.hidden_items",this.hass.language,"number",this._getFilteredItems())}
`:""}
`:q``}_toggleFilterMenu(e){const t=e.target;this._menu.anchor=t,this._menu.open?this._menu.close():(this.filterSelection=Object.entries(this.filterConfig).reduce((e,[t,a])=>Object.assign(Object.assign({},e),{[t]:ji(a,["value"])}),{}),this._menu.show())}renderFilterMenu(){return this.filterConfig&&this.filterSelection?q`
{this._menu.close(),setTimeout(()=>this._menu.anchor.blur(),50)}}
>
${Object.keys(this.filterConfig).map(e=>{if(this.filterConfig[e].binary)return q`
this._updateFilterSelection(e,t.target.checked)}
?checked=${this.filterSelection[e].value.length}
>
${this.filterConfig[e].name}
`;let t=this.filterConfig[e].items;t=t.map(t=>{var a;return t.badge&&"function"==typeof t.badge?{...t,badge:t.badge(null===(a=this.data)||void 0===a?void 0:a.filter(t=>this.filterTableData(t,Si(this.filterSelection,e))))}:t});const a=this.filterSelection[e].value;return q`
${this.filterConfig[e].name}
this._updateFilterSelection(e,t.detail)}
.value=${a}
>
`})}
`:q``}_updateFilterSelection(e,t){"boolean"==typeof t&&(t=t?this.filterConfig[e].items[0].value:[],1==Object.keys(this.filterConfig).length&&(this._menu.close(),setTimeout(()=>this._menu.anchor.blur(),50))),this.filterSelection=Object.assign(Object.assign({},this.filterSelection),{[e]:{value:t}})}_clearFilters(){Object.keys(this.filterConfig).forEach(e=>{this.filterConfig=Object.assign(Object.assign({},this.filterConfig),{[e]:Object.assign(Object.assign({},this.filterConfig[e]),{value:[]})})})}_applyFilterSelection(){Object.keys(this.filterConfig).forEach(e=>{this.filterConfig=Object.assign(Object.assign({},this.filterConfig),{[e]:Object.assign(Object.assign({},this.filterConfig[e]),this.filterSelection[e])})})}};us.styles=o`
:host {
width: 100%;
}
div.table {
display: inline-flex;
flex-direction: column;
box-sizing: border-box;
width: 100%;
}
div.table .header {
font-weight: bold;
}
div.table-row {
display: flex;
width: 100%;
height: 52px;
border-top: 1px solid var(--divider-color);
flex-direction: row;
position: relative;
}
div.table-cell {
align-self: center;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 0;
box-sizing: border-box;
}
div.table-cell.text {
padding: 4px 16px;
}
div.table-cell.grow {
flex-grow: 1;
flex-shrink: 1;
}
div.table-cell > ha-switch {
width: 68px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
}
div.table-cell.center {
display: flex;
align-items: center;
justify-content: center;
}
div.table-cell.right {
display: flex;
align-items: center;
justify-content: flex-end;
}
div.table-cell > ha-icon-button {
color: var(--secondary-text-color);
}
div.table-cell > * {
transition: color 0.2s ease-in-out;
}
div.table .header div.table-cell span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
div.table-row.selectable {
cursor: pointer;
}
.table-row::before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
opacity: 0.12;
pointer-events: none;
content: '';
border-radius: 4px;
}
div.table-row.selectable:hover::before {
background-color: rgba(var(--rgb-primary-text-color), 0.5);
}
div.table-row.warning::before {
background-color: var(--error-color);
opacity: 0.06;
}
div.table-row.warning:hover::before {
background-color: var(--error-color);
opacity: 0.12;
}
div.table-row.warning span {
color: var(--error-color);
}
ha-icon {
color: var(--state-icon-color);
padding: 8px;
}
.secondary {
color: var(--secondary-text-color);
display: flex;
padding-top: 4px;
}
a,
a:visited {
color: var(--primary-color);
}
span.disabled {
color: var(--secondary-text-color);
}
span.secondary.disabled {
color: var(--disabled-text-color);
}
ha-icon.disabled {
color: var(--state-unavailable-color);
}
div.table-filter {
display: flex;
width: 100%;
min-height: 52px;
border-top: 1px solid var(--divider-color);
box-sizing: border-box;
padding: 2px 8px;
flex: 1;
position: relative;
flex-direction: row;
align-items: center;
}
mwc-menu .header {
display: flex;
padding: 8px 16px;
font-weight: bold;
}
mwc-menu ha-icon-button {
position: absolute;
top: 8px;
right: 8px;
}
div.dropdown-item {
display: flex;
flex-direction: column;
flex-shrink: 0;
padding: 8px 16px;
width: 100%;
min-height: 52px;
box-sizing: border-box;
}
div.dropdown-item .name {
display: inline-flex;
}
div.dropdown-item alarmo-chips {
display: flex;
flex-direction: row;
}
div.dropdown-item.checkbox {
flex-direction: row;
align-items: center;
}
ha-button-menu mwc-button {
margin-left: 16px;
}
`,t([le()],us.prototype,"hass",void 0),t([le()],us.prototype,"columns",void 0),t([le()],us.prototype,"data",void 0),t([de()],us.prototype,"filterConfig",void 0),t([de()],us.prototype,"filterSelection",void 0),t([le({type:Boolean})],us.prototype,"selectable",void 0),t([ce("mwc-menu",!0)],us.prototype,"_menu",void 0),us=t([re("alarmo-table")],us);let ms=class extends ne{async showDialog(e){this._params=e,await this.updateComplete}async closeDialog(){this._params&&this._params.cancel(),this._params=void 0}render(){return this._params?q`
${this._params.title}
${this._params.description}
${this.hass.localize("ui.dialogs.generic.cancel")}
${this.hass.localize("ui.dialogs.generic.ok")}
`:q``}confirmClick(){this._params.confirm()}cancelClick(){this._params.cancel()}static get styles(){return o`
${qi}
div.wrapper {
color: var(--primary-text-color);
}
`}};t([le({attribute:!1})],ms.prototype,"hass",void 0),t([de()],ms.prototype,"_params",void 0),ms=t([re("confirm-delete-dialog")],ms);var ps=Object.freeze({__proto__:null,get ConfirmDeleteDialog(){return ms}});let gs=class extends(et(ne)){constructor(){super(...arguments),this.areas={},this.sensors={},this.automations={},this.name=""}hassSubscribe(){return this._fetchData(),[this.hass.connection.subscribeMessage(()=>this._fetchData(),{type:"alarmo_config_updated"})]}async _fetchData(){this.hass&&(this.areas=await Ze(this.hass),this.sensors=await Fe(this.hass),this.automations=await He(this.hass))}async showDialog(e){await this._fetchData(),this._params=e,e.area_id&&(this.area_id=e.area_id,this.name=this.areas[this.area_id].name),await this.updateComplete}async closeDialog(){this._params=void 0,this.area_id=void 0,this.name=""}render(){return this._params?q`
${this.area_id?gi("panels.general.dialogs.edit_area.title",this.hass.language,"{area}",this.areas[this.area_id].name):gi("panels.general.dialogs.create_area.title",this.hass.language)}
this.name=e.target.value}
value="${this.name}"
>
${this.area_id?q`
${gi("panels.general.dialogs.edit_area.name_warning",this.hass.language)}
`:""}
${this.area_id?"":q`
Object({value:e.area_id,name:e.name}))}
value=${this.selectedArea}
label="${gi("panels.general.dialogs.create_area.fields.copy_from",this.hass.language)}"
clearable=${!0}
@value-changed=${e=>this.selectedArea=e.target.value}
>
`}
${this.hass.localize("ui.common.save")}
${this.area_id?q`
${this.hass.localize("ui.common.delete")}
`:""}
`:q``}saveClick(e){const t=this.name.trim();if(!t.length)return;let a={name:t};this.area_id?a=Object.assign(Object.assign({},a),{area_id:this.area_id}):this.selectedArea&&(a=Object.assign(Object.assign({},a),{modes:Object.assign({},this.areas[this.selectedArea].modes)})),Je(this.hass,a).catch(t=>Di(t,e)).then(()=>{this.closeDialog()})}async deleteClick(e){if(!this.area_id)return;const t=Object.values(this.sensors).filter(e=>e.area==this.area_id).length,a=Object.values(this.automations).filter(e=>{var t;return null===(t=e.triggers)||void 0===t?void 0:t.map(e=>e.area).includes(this.area_id)}).length;let i=!1;var s,n;i=!t&&!a||await new Promise(i=>{De(e.target,"show-dialog",{dialogTag:"confirm-delete-dialog",dialogImport:()=>Promise.resolve().then((function(){return ps})),dialogParams:{title:gi("panels.general.dialogs.remove_area.title",this.hass.language),description:gi("panels.general.dialogs.remove_area.description",this.hass.language,"sensors",String(t),"automations",String(a)),cancel:()=>i(!1),confirm:()=>i(!0)}})}),i&&(s=this.hass,n=this.area_id,s.callApi("POST","alarmo/area",{area_id:n,remove:!0})).catch(t=>Di(t,e)).then(()=>{this.closeDialog()})}static get styles(){return o`
${qi}
div.wrapper {
color: var(--primary-text-color);
}
span.note {
color: var(--secondary-text-color);
}
ha-textfield {
display: block;
}
alarmo-select {
margin-top: 10px;
}
`}};t([le({attribute:!1})],gs.prototype,"hass",void 0),t([de()],gs.prototype,"_params",void 0),t([le()],gs.prototype,"areas",void 0),t([le()],gs.prototype,"sensors",void 0),t([le()],gs.prototype,"automations",void 0),t([le()],gs.prototype,"name",void 0),t([le()],gs.prototype,"area_id",void 0),t([le()],gs.prototype,"selectedArea",void 0),gs=t([re("create-area-dialog")],gs);var vs=Object.freeze({__proto__:null,get CreateAreaDialog(){return gs}});let fs=class extends(et(ne)){constructor(){super(...arguments),this.areas={},this.sensors={},this.automations={}}hassSubscribe(){return this._fetchData(),[this.hass.connection.subscribeMessage(()=>this._fetchData(),{type:"alarmo_config_updated"})]}async _fetchData(){this.hass&&(this.areas=await Ze(this.hass),this.sensors=await Fe(this.hass),this.automations=await He(this.hass))}render(){if(!this.hass)return q``;const e=Object.values(this.areas);e.sort(Pi);const t={actions:{width:"48px"},name:{title:this.hass.localize("ui.components.area-picker.add_dialog.name"),width:"40%",grow:!0,text:!0},remarks:{title:gi("panels.general.cards.areas.table.remarks",this.hass.language),width:"60%",hide:this.narrow,text:!0}},a=Object.values(e).map(t=>{const a=Object.values(this.sensors).filter(e=>e.area==t.area_id).length,i=1==Object.values(e).length?Object.values(this.automations).filter(e=>{var a,i;return(null===(a=e.triggers)||void 0===a?void 0:a.map(e=>e.area).includes(t.area_id))||!(null===(i=e.triggers)||void 0===i?void 0:i.map(e=>e.area).length)}).length:Object.values(this.automations).filter(e=>{var a;return null===(a=e.triggers)||void 0===a?void 0:a.map(e=>e.area).includes(t.area_id)}).length,s=`${gi("panels.general.cards.areas.table.summary_sensors",this.hass.language,"number",a)}`,n=`${gi("panels.general.cards.areas.table.summary_automations",this.hass.language,"number",i)}`;return{id:t.area_id,actions:q`
this.editClick(e,t.area_id)} .path=${"M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z"}>
`,name:xi(t.name),remarks:ds(gi("panels.general.cards.areas.table.summary",this.hass.language,"summary_sensors",s,"summary_automations",n))}});return q`
${gi("panels.general.cards.areas.description",this.hass.language)}
${gi("panels.general.cards.areas.no_items",this.hass.language)}
${gi("panels.general.cards.areas.actions.add",this.hass.language)}
`}addClick(e){const t=e.target;De(t,"show-dialog",{dialogTag:"create-area-dialog",dialogImport:()=>Promise.resolve().then((function(){return vs})),dialogParams:{}})}editClick(e,t){const a=e.target;De(a,"show-dialog",{dialogTag:"create-area-dialog",dialogImport:()=>Promise.resolve().then((function(){return vs})),dialogParams:{area_id:t}})}};fs.styles=qi,t([le()],fs.prototype,"narrow",void 0),t([le()],fs.prototype,"path",void 0),t([le()],fs.prototype,"config",void 0),t([le()],fs.prototype,"areas",void 0),t([le()],fs.prototype,"sensors",void 0),t([le()],fs.prototype,"automations",void 0),fs=t([re("area-config-card")],fs);let _s=class extends ne{constructor(){super(...arguments),this.name=""}async showDialog(e){this._params=e;const t=await Ge(this.hass);this.name=t.master.name||"",await this.updateComplete}async closeDialog(){this._params=void 0}render(){return this._params?q`
${gi("panels.general.dialogs.edit_master.title",this.hass.language)}
this.name=e.target.value}
value="${this.name}"
>
${gi("panels.general.dialogs.edit_area.name_warning",this.hass.language)}
${this.hass.localize("ui.common.save")}
${this.hass.localize("ui.common.cancel")}
`:q``}saveClick(){const e=this.name.trim();e.length&&Be(this.hass,{master:{enabled:!0,name:e}}).catch().then(()=>{this.closeDialog()})}static get styles(){return o`
div.wrapper {
color: var(--primary-text-color);
}
span.note {
color: var(--secondary-text-color);
}
ha-textfield {
display: block;
}
`}};t([le({attribute:!1})],_s.prototype,"hass",void 0),t([de()],_s.prototype,"_params",void 0),t([le()],_s.prototype,"name",void 0),_s=t([re("edit-master-dialog")],_s);var bs=Object.freeze({__proto__:null,get EditMasterDialog(){return _s}});let ys=class extends(et(ne)){constructor(){super(...arguments),this.areas={},this.automations={}}hassSubscribe(){return this._fetchData(),[this.hass.connection.subscribeMessage(()=>this._fetchData(),{type:"alarmo_config_updated"})]}async _fetchData(){this.hass&&(this.config=await Ge(this.hass),this.areas=await Ze(this.hass),this.automations=await He(this.hass),this.data=ji(this.config,["trigger_time","disarm_after_trigger","mqtt","master"]))}firstUpdated(){(async()=>{await Ie()})()}render(){var e,t,a,i,s,n,r,o;return this.hass&&this.config&&this.data?"mqtt_configuration"==this.path.subpage?q`
`:this.path.params.edit_area?q`
`:q`
${gi("panels.general.cards.general.description",this.hass.language)}
${gi("panels.general.cards.general.fields.disarm_after_trigger.heading",this.hass.language)}
${gi("panels.general.cards.general.fields.disarm_after_trigger.description",this.hass.language)}
{this.saveData({disarm_after_trigger:e.target.checked})}}
>
${gi("panels.general.cards.general.fields.enable_mqtt.heading",this.hass.language)}
${gi("panels.general.cards.general.fields.enable_mqtt.description",this.hass.language)}
{this.saveData({mqtt:{...this.data.mqtt,enabled:e.target.checked}})}}
>
${(null===(i=null===(a=this.data)||void 0===a?void 0:a.mqtt)||void 0===i?void 0:i.enabled)?q`
Pe(0,Ui("general","mqtt_configuration"),!0)}
>
${gi("panels.general.cards.general.actions.setup_mqtt",this.hass.language)}
`:""}
${Object.keys(this.areas).length>=2?q`
${gi("panels.general.cards.general.fields.enable_master.heading",this.hass.language)}
${gi("panels.general.cards.general.fields.enable_master.description",this.hass.language)}
=2}
?disabled=${Object.keys(this.areas).length<2}
@change=${this.toggleEnableMaster}
>
`:""}
${(null===(o=null===(r=this.data)||void 0===r?void 0:r.master)||void 0===o?void 0:o.enabled)&&Object.keys(this.areas).length>=2?q`
${gi("panels.general.cards.general.actions.setup_master",this.hass.language)}
`:""}
`:q``}setupMasterClick(e){const t=e.target;De(t,"show-dialog",{dialogTag:"edit-master-dialog",dialogImport:()=>Promise.resolve().then((function(){return bs})),dialogParams:{}})}async toggleEnableMaster(e){const t=e.target;let a=t.checked;if(!a){const i=Object.values(this.automations).filter(e=>e.triggers.some(e=>!e.area));if(i.length){await new Promise(e=>{De(t,"show-dialog",{dialogTag:"confirm-delete-dialog",dialogImport:()=>Promise.resolve().then((function(){return ps})),dialogParams:{title:gi("panels.general.dialogs.disable_master.title",this.hass.language),description:gi("panels.general.dialogs.disable_master.description",this.hass.language,"automations",String(i.length)),cancel:()=>e(!1),confirm:()=>e(!0)}})})?!a&&i.length&&i.forEach(t=>{Xe(this.hass,t.automation_id).catch(t=>Di(t,e))}):(a=!0,t.checked=!0)}}this.saveData({master:Object.assign(Object.assign({},this.data.master),{enabled:a})})}saveData(e){this.hass&&this.data&&(this.data=Object.assign(Object.assign({},this.data),e),Be(this.hass,this.data).catch(e=>Di(e,this.shadowRoot.querySelector("ha-card"))).then())}};ys.styles=qi,t([le()],ys.prototype,"narrow",void 0),t([le()],ys.prototype,"path",void 0),t([le()],ys.prototype,"data",void 0),t([le()],ys.prototype,"config",void 0),t([le()],ys.prototype,"areas",void 0),t([le()],ys.prototype,"automations",void 0),ys=t([re("alarm-view-general")],ys);const ws=(e,t)=>{if("binary_sensor"==function(e){const t="string"==typeof e?e:e.entity_id;return String(t.split(".").shift())}(e.entity_id)){if(t)return!0;const a=e.attributes.device_class;return!!a&&!!["carbon_monoxide","door","garage_door","gas","heat","lock","moisture","motion","moving","occupancy","opening","presence","safety","smoke","sound","vibration","window"].includes(a)}return!1},ks=e=>{switch(e.attributes.device_class){case"door":case"garage_door":case"lock":return bi.Door;case"window":return bi.Window;case"carbon_monoxide":case"gas":case"heat":case"moisture":case"smoke":case"safety":return bi.Environmental;case"motion":case"moving":case"occupancy":case"presence":return bi.Motion;case"sound":case"opening":case"vibration":return bi.Tamper;default:return}},$s=e=>{const t=t=>t.filter(t=>e.includes(t));return{[bi.Door]:{modes:t([ki.ArmedAway,ki.ArmedHome,ki.ArmedNight,ki.ArmedVacation]),always_on:!1,allow_open:!1,arm_on_close:!1,use_entry_delay:!0,use_exit_delay:!1},[bi.Window]:{modes:t([ki.ArmedAway,ki.ArmedHome,ki.ArmedNight,ki.ArmedVacation]),always_on:!1,allow_open:!1,arm_on_close:!1,use_entry_delay:!1,use_exit_delay:!1},[bi.Motion]:{modes:t([ki.ArmedAway,ki.ArmedVacation]),always_on:!1,allow_open:!0,arm_on_close:!1,use_entry_delay:!0,use_exit_delay:!0},[bi.Tamper]:{modes:t([ki.ArmedAway,ki.ArmedHome,ki.ArmedNight,ki.ArmedVacation,ki.ArmedCustom]),always_on:!1,allow_open:!1,arm_on_close:!1,use_entry_delay:!1,use_exit_delay:!1},[bi.Environmental]:{modes:t([ki.ArmedAway,ki.ArmedHome,ki.ArmedNight,ki.ArmedVacation,ki.ArmedCustom]),always_on:!0,allow_open:!1,arm_on_close:!1,use_entry_delay:!1,use_exit_delay:!1}}};let As=class extends ne{async showDialog(e){this._params=e,await this.updateComplete}async closeDialog(){this._params=void 0}render(){return this._params?q`
${this.hass.localize("state_badge.default.error")}
${this._params.error||""}
${this.hass.localize("ui.dialogs.generic.ok")}
`:q``}static get styles(){return o`
div.wrapper {
color: var(--primary-text-color);
}
`}};t([le({attribute:!1})],As.prototype,"hass",void 0),t([de()],As.prototype,"_params",void 0),As=t([re("error-dialog")],As);var xs=Object.freeze({__proto__:null,get ErrorDialog(){return As}});let Os=class extends(et(ne)){constructor(){super(...arguments),this.sensorGroups={},this.sensors={}}hassSubscribe(){return this._fetchData(),[this.hass.connection.subscribeMessage(()=>this._fetchData(),{type:"alarmo_config_updated"})]}async _fetchData(){this.hass&&(this.sensorGroups=await Ye(this.hass),this.sensors=await Fe(this.hass))}async showDialog(e){await this._fetchData(),this._params=e,e.group_id&&Object.keys(this.sensorGroups).includes(e.group_id)?this.data=Object.assign({},this.sensorGroups[e.group_id]):this.data={name:"",entities:[],timeout:600},await this.updateComplete}async closeDialog(){this._params=void 0}render(){return this._params?q`
${gi("panels.sensors.dialogs.create_group.fields.name.heading",this.hass.language)}
${gi("panels.sensors.dialogs.create_group.fields.name.description",this.hass.language)}
this.data={...this.data,name:String(e.target.value).trim()}}
value="${this.data.name}"
>
${gi("panels.sensors.dialogs.create_group.fields.sensors.heading",this.hass.language)}
${gi("panels.sensors.dialogs.create_group.fields.sensors.description",this.hass.language)}
${this.renderSensorOptions()}
${gi("panels.sensors.dialogs.create_group.fields.timeout.heading",this.hass.language)}
${gi("panels.sensors.dialogs.create_group.fields.timeout.description",this.hass.language)}
this.data={...this.data,timeout:Number(e.target.value)}}
>
${this.hass.localize("ui.common.save")}
${this.data.group_id?q`
${this.hass.localize("ui.common.delete")}
`:""}
`:q``}renderHeader(){return q`
`}renderSensorOptions(){const e=Object.keys(this.sensors).filter(e=>!Ci(this.sensors[e].group)||this.sensors[e].group===this.data.group_id).map(e=>{const t=this.hass.states[e],a=Object.entries(bi).find(([,t])=>t==this.sensors[e].type)[0];return{value:e,name:xi(Oi(t)),icon:yi[a]}});return e.sort(Pi),e.length?q`
this.data={...this.data,entities:e.detail}}
>
`:gi("panels.sensors.cards.sensors.no_items",this.hass.language)}saveClick(e){var t,a;this.data.name.length&&(this.data.group_id&&this.data.name==this.sensorGroups[this.data.group_id].name||!Object.values(this.sensorGroups).find(e=>e.name.toLowerCase()==this.data.name.toLowerCase()))?this.data.entities.length<2?Ni(e,gi("panels.sensors.dialogs.create_group.errors.insufficient_sensors",this.hass.language)):(t=this.hass,a=this.data,t.callApi("POST","alarmo/sensor_groups",a)).catch(t=>Di(t,e)).then(()=>{this.closeDialog()}):Ni(e,gi("panels.sensors.dialogs.create_group.errors.invalid_name",this.hass.language))}deleteClick(e){var t,a;this.data.group_id&&(t=this.hass,a=this.data.group_id,t.callApi("POST","alarmo/sensor_groups",{group_id:a,remove:!0})).catch(t=>Di(t,e)).then(()=>{this.closeDialog()})}static get styles(){return o`
${Ri}
div.wrapper {
color: var(--primary-text-color);
}
mwc-button.warning {
--mdc-theme-primary: var(--error-color);
}
`}};t([le({attribute:!1})],Os.prototype,"hass",void 0),t([de()],Os.prototype,"_params",void 0),t([le()],Os.prototype,"sensorGroups",void 0),t([le()],Os.prototype,"sensors",void 0),t([le()],Os.prototype,"data",void 0),Os=t([re("create-sensor-group-dialog")],Os);var Es=Object.freeze({__proto__:null,get CreateSensorGroupDialog(){return Os}});let Ts=class extends(et(ne)){constructor(){super(...arguments),this.sensorGroups={},this.sensors={}}hassSubscribe(){return this._fetchData(),[this.hass.connection.subscribeMessage(()=>this._fetchData(),{type:"alarmo_config_updated"})]}async _fetchData(){this.hass&&(this.sensorGroups=await Ye(this.hass),this.sensors=await Fe(this.hass))}async showDialog(e){await this._fetchData(),this._params=e,await this.updateComplete}async closeDialog(){this._params=void 0}render(){return this._params?q`
${gi("panels.sensors.dialogs.manage_groups.description",this.hass.language)}
${Object.keys(this.sensorGroups).length?Object.values(this.sensorGroups).map(e=>this.renderGroup(e)):gi("panels.sensors.dialogs.manage_groups.no_items",this.hass.language)}
${gi("panels.sensors.dialogs.manage_groups.actions.new_group",this.hass.language)}
`:q``}renderHeader(){return q`
`}renderGroup(e){return q`
this.editGroupClick(t,e.group_id)}
>
${e.name}
${gi("panels.general.cards.areas.table.summary_sensors",this.hass.language,"{number}",String(e.entities.length))}
`}createGroupClick(e){const t=e.target;De(t,"show-dialog",{dialogTag:"create-sensor-group-dialog",dialogImport:()=>Promise.resolve().then((function(){return Es})),dialogParams:{}})}editGroupClick(e,t){const a=e.target;De(a,"show-dialog",{dialogTag:"create-sensor-group-dialog",dialogImport:()=>Promise.resolve().then((function(){return Es})),dialogParams:{group_id:t}})}static get styles(){return o`
${Ri}
div.wrapper {
color: var(--primary-text-color);
}
div.container {
display: flex;
flex-wrap: wrap;
}
ha-card {
width: 100%;
text-align: center;
margin: 4px;
box-sizing: border-box;
padding: 8px;
color: var(--primary-text-color);
font-size: 16px;
cursor: pointer;
display: flex;
flex-direction: row;
}
ha-card:hover {
background: rgba(var(--rgb-secondary-text-color), 0.1);
}
ha-card ha-icon {
--mdc-icon-size: 24px;
display: flex;
flex: 0 0 40px;
margin: 0px 10px;
align-items: center;
color: var(--state-icon-color);
}
ha-card ha-icon-button {
--mdc-icon-size: 24px;
display: flex;
flex: 0 0 40px;
margin: 0px 10px;
align-items: center;
}
ha-card div {
display: flex;
flex-wrap: wrap;
flex: 1;
}
ha-card span {
display: flex;
flex: 0 0 100%;
}
ha-card span.description {
color: var(--secondary-text-color);
}
mwc-button ha-icon {
padding-right: 11px;
}
`}};t([le({attribute:!1})],Ts.prototype,"hass",void 0),t([de()],Ts.prototype,"_params",void 0),t([le()],Ts.prototype,"sensorGroups",void 0),t([le()],Ts.prototype,"sensors",void 0),Ts=t([re("manage-sensor-groups-dialog")],Ts);var js=Object.freeze({__proto__:null,get ManageSensorGroupsDialog(){return Ts}});let Ss=class extends(et(ne)){constructor(){super(...arguments),this.showBypassModes=!1}hassSubscribe(){return this._fetchData(),[this.hass.connection.subscribeMessage(()=>this._fetchData(),{type:"alarmo_config_updated"})]}async _fetchData(){var e;if(!this.hass)return;const t=await Ze(this.hass);this.areas=t;const a=await Ye(this.hass);this.sensorGroups=a;const i=await Fe(this.hass);this.data=Object.keys(i).includes(this.item)?i[this.item]:void 0,this.data&&!(null===(e=this.data)||void 0===e?void 0:e.area)&&1==Object.keys(t).length&&(this.data=Object.assign(Object.assign({},this.data),{area:Object.keys(this.areas)[0]}))}render(){if(!this.data)return q``;this.hass.states[this.data.entity_id];return q`
${gi("panels.sensors.cards.editor.description",this.hass.language,"{entity}",Oi(this.hass.states[this.item]))}
${Object.keys(this.areas).length>1?q`
${gi("panels.sensors.cards.editor.fields.area.heading",this.hass.language)}
${gi("panels.sensors.cards.editor.fields.area.description",this.hass.language)}
Object({value:e.area_id,name:e.name}))}
value=${this.data.area}
label=${gi("panels.sensors.cards.editor.fields.area.heading",this.hass.language)}
@value-changed=${e=>this.data={...this.data,area:e.target.value}}
?invalid=${!this.data.area}
>
`:""}
${gi("panels.sensors.cards.editor.fields.device_type.heading",this.hass.language)}
${gi("panels.sensors.cards.editor.fields.device_type.description",this.hass.language)}
e!=bi.Other).map(([t,a])=>Object({value:a,name:gi(`panels.sensors.cards.editor.fields.device_type.choose.${a}.name`,e.language),description:gi(`panels.sensors.cards.editor.fields.device_type.choose.${a}.description`,e.language),icon:yi[t]}))}
label=${gi("panels.sensors.cards.editor.fields.device_type.heading",this.hass.language)}
clearable=${!0}
icons=${!0}
value=${this.data.type}
@value-changed=${e=>this.setType(e.target.value||bi.Other)}
>
3}>
${gi("panels.sensors.cards.editor.fields.modes.heading",this.hass.language)}
${gi("panels.sensors.cards.editor.fields.modes.description",this.hass.language)}
${this.modesByArea(this.data.area).map(e=>q`
{this.setMode(e)}}
?disabled=${this.data.always_on}
>
${gi("common.modes_short."+e,this.hass.language)}
`)}
${gi("panels.sensors.cards.editor.fields.group.heading",this.hass.language)}
${gi("panels.sensors.cards.editor.fields.group.description",this.hass.language)}
${Object.keys(this.sensorGroups).length?q`
{this.data={...this.data,group:e.detail.value}}}
>
`:""}
${gi("panels.sensors.cards.editor.actions.setup_groups",this.hass.language)}
${!this.data.type||[bi.Environmental,bi.Other].includes(this.data.type)?q`
${gi("panels.sensors.cards.editor.fields.always_on.heading",this.hass.language)}
${gi("panels.sensors.cards.editor.fields.always_on.description",this.hass.language)}
this._SetData({always_on:e.target.checked})}
>
`:""}
${!this.data.type||[bi.Window,bi.Door,bi.Motion,bi.Other].includes(this.data.type)?q`
${gi("panels.sensors.cards.editor.fields.use_exit_delay.heading",this.hass.language)}
${gi("panels.sensors.cards.editor.fields.use_exit_delay.description",this.hass.language)}
this._SetData({use_exit_delay:e.target.checked})}
>
${this.data.type&&![bi.Motion,bi.Other].includes(this.data.type)||!this.data.use_exit_delay?"":q`
${gi("panels.sensors.cards.editor.fields.allow_open.heading",this.hass.language)}
${gi("panels.sensors.cards.editor.fields.allow_open.description",this.hass.language)}
this._SetData({allow_open:e.target.checked})}
>
`}
`:""}
${!this.data.type||[bi.Window,bi.Door,bi.Motion,bi.Other].includes(this.data.type)?q`
${gi("panels.sensors.cards.editor.fields.use_entry_delay.heading",this.hass.language)}
${gi("panels.sensors.cards.editor.fields.use_entry_delay.description",this.hass.language)}
this._SetData({use_entry_delay:e.target.checked})}
>
`:""}
${!this.data.type||[bi.Door,bi.Other].includes(this.data.type)?q`
${gi("panels.sensors.cards.editor.fields.arm_on_close.heading",this.hass.language)}
${gi("panels.sensors.cards.editor.fields.arm_on_close.description",this.hass.language)}
this._SetData({arm_on_close:e.target.checked})}
>
`:""}
${!this.data.type||[bi.Window,bi.Door,bi.Other].includes(this.data.type)?q`
${gi("panels.sensors.cards.editor.fields.auto_bypass.heading",this.hass.language)}
${gi("panels.sensors.cards.editor.fields.auto_bypass.description",this.hass.language)}
this._SetData({auto_bypass:e.target.checked})}
>
${this.data.auto_bypass?q`
${gi("panels.sensors.cards.editor.fields.auto_bypass.modes",this.hass.language)}
${this.modesByArea(this.data.area).map(e=>q`
{this.setBypassMode(e)}}
>
${gi("common.modes_short."+e,this.hass.language)}
`)}
`:""}
`:""}
${gi("panels.sensors.cards.editor.fields.trigger_unavailable.heading",this.hass.language)}
${gi("panels.sensors.cards.editor.fields.trigger_unavailable.description",this.hass.language)}
this._SetData({trigger_unavailable:e.target.checked})}
>
${this.hass.localize("ui.common.save")}
${gi("panels.sensors.cards.editor.actions.remove",this.hass.language)}
`;var e}modesByArea(e){const t=Object.keys(this.areas).reduce((e,t)=>Object.assign(e,{[t]:Object.entries(this.areas[t].modes).filter(([,e])=>e.enabled).map(([e])=>e)}),{});return e?t[e]:Object.values(t).reduce((e,t)=>e.filter(e=>t.includes(e)))}_SetData(e){if(this.data)for(const[t,a]of Object.entries(e))switch(t){case"always_on":this.data=Object.assign(Object.assign({},this.data),{always_on:1==a}),a&&(this.data=Object.assign(Object.assign({},this.data),{arm_on_close:!1,use_exit_delay:!1,use_entry_delay:!1,allow_open:!1,auto_bypass:!1}));break;case"use_entry_delay":this.data=Object.assign(Object.assign({},this.data),{use_entry_delay:1==a});break;case"use_exit_delay":this.data=Object.assign(Object.assign({},this.data),{use_exit_delay:1==a}),a&&(this.data=Object.assign(Object.assign({},this.data),{allow_open:!1}));break;case"arm_on_close":this.data=Object.assign(Object.assign({},this.data),{arm_on_close:1==a}),a&&(this.data=Object.assign(Object.assign({},this.data),{always_on:!1,allow_open:!1}));break;case"allow_open":this.data=Object.assign(Object.assign({},this.data),{allow_open:1==a}),a&&(this.data=Object.assign(Object.assign({},this.data),{arm_on_close:!1,always_on:!1,use_exit_delay:!0}));break;case"auto_bypass":this.data=Object.assign(Object.assign({},this.data),{auto_bypass:1==a}),a&&(this.data=Object.assign(Object.assign({},this.data),{always_on:!1}));break;case"trigger_unavailable":this.data=Object.assign(Object.assign({},this.data),{trigger_unavailable:1==a})}}setMode(e){this.data&&(this.data=Object.assign(Object.assign({},this.data),{modes:this.data.modes.includes(e)?Ti(this.data.modes,e):Ei(this.data.modes.concat([e]))}))}setBypassMode(e){this.data&&(this.data=Object.assign(Object.assign({},this.data),{auto_bypass_modes:this.data.auto_bypass_modes.includes(e)?Ti(this.data.auto_bypass_modes,e):Ei(this.data.auto_bypass_modes.concat([e]))}))}setType(e){if(!this.data)return;const t=e!=bi.Other?$s(this.modesByArea(this.data.area))[e]:{};this.data=Object.assign(Object.assign(Object.assign({},this.data),{type:e}),t)}deleteClick(e){var t,a;(t=this.hass,a=this.item,t.callApi("POST","alarmo/sensors",{entity_id:a,remove:!0})).catch(t=>Di(t,e)).then(()=>{this.cancelClick()})}saveClick(e){if(!this.data)return;const t=[];this.data=Object.assign(Object.assign({},this.data),{auto_bypass_modes:this.data.auto_bypass_modes.filter(e=>this.data.modes.includes(e))}),this.data.area||t.push(gi("panels.sensors.cards.editor.errors.no_area",this.hass.language)),this.data.modes.length||this.data.always_on||t.push(gi("panels.sensors.cards.editor.errors.no_modes",this.hass.language)),this.data.auto_bypass&&!this.data.auto_bypass_modes.length&&t.push(gi("panels.sensors.cards.editor.errors.no_auto_bypass_modes",this.hass.language)),t.length?Ni(e,q`
${gi("panels.sensors.cards.editor.errors.description",this.hass.language)}
`):Ke(this.hass,Object.assign({},this.data)).catch(t=>Di(t,e)).then(()=>{this.cancelClick()})}cancelClick(){Pe(0,Ui("sensors"),!0)}manageGroupsClick(e){const t=e.target;De(t,"show-dialog",{dialogTag:"manage-sensor-groups-dialog",dialogImport:()=>Promise.resolve().then((function(){return js})),dialogParams:{}})}getSensorGroups(){return Object.keys(this.sensorGroups).map(e=>Object({value:e,name:this.sensorGroups[e].name}))}};Ss.styles=qi,t([le()],Ss.prototype,"hass",void 0),t([le()],Ss.prototype,"narrow",void 0),t([le()],Ss.prototype,"item",void 0),t([le()],Ss.prototype,"data",void 0),t([le()],Ss.prototype,"showBypassModes",void 0),Ss=t([re("sensor-editor-card")],Ss);const Cs=e=>Object.keys(e.modes).filter(t=>e.modes[t].enabled),Ms=e=>{let t=[];return Object.values(e).forEach(e=>{t=[...t,...Cs(e)]}),t=Ei(t),t.sort((e,t)=>{const a=Object.values(ki);return a.findIndex(t=>t==e)-a.findIndex(e=>e==t)}),t},Ns="no_area";let Ds=class extends(et(ne)){hassSubscribe(){return this._fetchData(),[this.hass.connection.subscribeMessage(()=>this._fetchData(),{type:"alarmo_config_updated"})]}async _fetchData(){this.hass&&(this.areas=await Ze(this.hass),this.sensors=await Fe(this.hass))}async firstUpdated(){this.path&&2==this.path.length&&"filter"==this.path[0]&&(this.selectedArea=this.path[1])}render(){return this.hass&&this.areas&&this.sensors?q`
${gi("panels.sensors.cards.sensors.description",this.hass.language)}
{Pe(0,Ui("sensors",{params:{edit:e.detail.id}}),!0)}}
>
${gi("panels.sensors.cards.sensors.table.no_items",this.hass.language)}
`:q``}tableColumns(){const e=()=>q`
${gi("panels.sensors.cards.sensors.table.no_area_warning",this.hass.language)}
`;return{icon:{width:"40px",renderer:t=>{const a=this.hass.states[t.entity_id],i=Object.keys(bi).find(e=>bi[e]==t.type),s=a?yi[i]:"hass:help-circle-outline";return t.area==Ns?q`
${e()}
`:q`
${a?gi(`panels.sensors.cards.editor.fields.device_type.choose.${t.type}.name`,this.hass.language):this.hass.localize("state_badge.default.entity_not_found")}
`}},name:{title:this.hass.localize("ui.components.entity.entity-picker.entity"),width:"60%",grow:!0,text:!0,renderer:t=>q`
${t.area==Ns?e():""}
${t.name}
${t.entity_id}
`},modes:{title:gi("panels.sensors.cards.sensors.table.arm_modes",this.hass.language),width:"25%",hide:this.narrow,text:!0,renderer:t=>q`
${t.area==Ns?e():""}
${t.always_on?gi("panels.sensors.cards.sensors.table.always_on",this.hass.language):t.modes.length?t.modes.map(e=>gi("common.modes_short."+e,this.hass.language)).join(", "):this.hass.localize("state_attributes.climate.preset_mode.none")}
`},enabled:{title:gi("common.enabled",this.hass.language),width:"68px",align:"center",renderer:e=>q`
{e.stopPropagation()}}
?checked=${e.enabled}
@change=${t=>this.toggleEnabled(t,e.entity_id)}
>
`}}}getTableData(){let e=Object.keys(this.sensors).map(e=>{const t=this.hass.states[e],a=this.sensors[e],i=a.area?Cs(this.areas[a.area]):Ms(this.areas);return Object.assign(Object.assign({},a),{id:e,name:Oi(t),modes:a.always_on?i:a.modes.filter(e=>i.includes(e)),warning:!a.area,area:a.area||Ns})});return e.sort(Pi),e}toggleEnabled(e,t){const a=e.target.checked;Ke(this.hass,{entity_id:t,enabled:a}).catch(t=>Di(t,e)).then()}removeCustomName(e){let t={entity_id:e,name:""};Ke(this.hass,t)}getTableFilterOptions(){let e=Object.values(this.areas).map(e=>Object({value:e.area_id,name:e.name,badge:t=>t.filter(t=>t.area==e.area_id).length})).sort(Pi);Object.values(this.sensors).filter(e=>!e.area).length&&(e=[{value:Ns,name:this.hass.localize("state_attributes.climate.preset_mode.none"),badge:e=>e.filter(e=>e.area==Ns).length},...e]);const t=Ms(this.areas).map(e=>Object({value:e,name:gi("common.modes_short."+e,this.hass.language),badge:t=>t.filter(t=>t.modes.includes(e)).length}));return{area:{name:gi("components.table.filter.item",this.hass.language,"name",gi("panels.actions.cards.new_action.fields.area.heading",this.hass.language)),items:e,value:this.selectedArea?[this.selectedArea]:[]},modes:{name:gi("components.table.filter.item",this.hass.language,"name",gi("panels.actions.cards.new_action.fields.mode.heading",this.hass.language)),items:t,value:this.selectedMode?[this.selectedMode]:[]}}}};Ds.styles=qi,t([le()],Ds.prototype,"hass",void 0),t([le()],Ds.prototype,"narrow",void 0),t([le()],Ds.prototype,"areas",void 0),t([le()],Ds.prototype,"sensors",void 0),t([le()],Ds.prototype,"selectedArea",void 0),t([le()],Ds.prototype,"selectedMode",void 0),t([le()],Ds.prototype,"path",void 0),Ds=t([re("sensors-overview-card")],Ds);let Ls=class extends(et(ne)){constructor(){super(...arguments),this.addSelection=[],this.areas={},this.sensors={}}hassSubscribe(){return this._fetchData(),[this.hass.connection.subscribeMessage(()=>this._fetchData(),{type:"alarmo_config_updated"})]}async _fetchData(){this.hass&&(this.areas=await Ze(this.hass))}async firstUpdated(){this.areas=await Ze(this.hass),this.sensors=await Fe(this.hass)}render(){const e={checkbox:{width:"48px",renderer:e=>q`
this.toggleSelect(t,e.id)}
?checked=${this.addSelection.includes(e.id)}
>
`},icon:{width:"40px",renderer:e=>q`
`},name:{title:this.hass.localize("ui.components.entity.entity-picker.entity"),width:"40%",grow:!0,text:!0,renderer:e=>q`
${xi(e.name)}
${e.id}
`},type:{title:gi("panels.sensors.cards.add_sensors.table.type",this.hass.language),width:"40%",hide:this.narrow,text:!0,renderer:e=>e.type?gi(`panels.sensors.cards.editor.fields.device_type.choose.${e.type}.name`,this.hass.language):this.hass.localize("state.default.unknown")}},t=((e,t,a=!1)=>{const i=Object.values(e.states).filter(e=>ws(e,a)).filter(e=>!t.includes(e.entity_id)).map(e=>Object({id:e.entity_id,name:Oi(e),icon:Ai(e)}));return i.sort(Pi),i})(this.hass,Object.keys(this.sensors),!0).map(e=>Object.assign(Object.assign({},e),{type:ks(this.hass.states[e.id]),isSupportedType:void 0!==ks(this.hass.states[e.id])?"true":"false"}));return q`
${gi("panels.sensors.cards.add_sensors.description",this.hass.language)}
${gi("panels.sensors.cards.add_sensors.no_items",this.hass.language)}
${gi("panels.sensors.cards.add_sensors.actions.add_to_alarm",this.hass.language)}
`}toggleSelect(e,t){const a=e.target.checked;this.addSelection=a&&!this.addSelection.includes(t)?[...this.addSelection,t]:a?this.addSelection:this.addSelection.filter(e=>e!=t)}addSelected(e){if(!this.hass)return;const t=Object.values(this.areas).map(e=>Object.entries(e.modes).filter(([,e])=>e.enabled).map(([e])=>e)).reduce((e,t)=>e.filter(e=>t.includes(e)));this.addSelection.map(e=>function(e,t){if(!e)return null;const a=Ce(e.entity_id);let i={entity_id:e.entity_id,modes:[],use_entry_delay:!0,use_exit_delay:!0,arm_on_close:!1,allow_open:!1,always_on:!1,auto_bypass:!1,auto_bypass_modes:[],trigger_unavailable:!1,type:bi.Other,enabled:!0};if("binary_sensor"==a){const a=ks(e);a&&(i=Object.assign(Object.assign(Object.assign({},i),{type:a}),$s(t)[a]))}return i}(this.hass.states[e],t)).map(e=>1==Object.keys(this.areas).length?Object.assign(e,{area:Object.keys(this.areas)[0]}):e).filter(e=>e).forEach(t=>{Ke(this.hass,t).catch(t=>Di(t,e)).then()}),this.addSelection=[]}getTableFilterOptions(){return{isSupportedType:{name:gi("panels.sensors.cards.add_sensors.actions.filter_supported",this.hass.language),items:[{value:"true",name:"true"}],value:["true"],binary:!0}}}};Ls.styles=qi,t([le()],Ls.prototype,"hass",void 0),t([le()],Ls.prototype,"narrow",void 0),t([le()],Ls.prototype,"addSelection",void 0),t([le()],Ls.prototype,"areas",void 0),t([le()],Ls.prototype,"sensors",void 0),Ls=t([re("add-sensors-card")],Ls);let zs=class extends ne{firstUpdated(){(async()=>{await Ie()})()}render(){var e,t;if(!this.hass)return q``;if(this.path.params.edit)return q`
`;{const a=null===(e=this.path.filter)||void 0===e?void 0:e.area,i=null===(t=this.path.filter)||void 0===t?void 0:t.mode;return q`
`}}};t([le()],zs.prototype,"hass",void 0),t([le()],zs.prototype,"narrow",void 0),t([le()],zs.prototype,"path",void 0),zs=t([re("alarm-view-sensors")],zs);let Ps=class extends ne{constructor(){super(...arguments),this.data={can_arm:!0,can_disarm:!0,is_override_code:!1},this.repeatCode="",this.areas={}}async firstUpdated(){if(this.users=await Ve(this.hass),this.areas=await Ze(this.hass),this.item){const e=this.users[this.item];this.data=Si(e,"code","code_format","code_length")}this.data=Object.assign(Object.assign({},this.data),{area_limit:(this.data.area_limit||[]).filter(e=>Object.keys(this.areas).includes(e))}),(this.data.area_limit||[]).length||(this.data=Object.assign(Object.assign({},this.data),{area_limit:Object.keys(this.areas)}))}render(){var e;return this.users?q`
${this.item?gi("panels.codes.cards.edit_user.description",this.hass.language,"{name}",this.users[this.item].name):gi("panels.codes.cards.new_user.description",this.hass.language)}
${gi("panels.codes.cards.new_user.fields.name.heading",this.hass.language)}
${gi("panels.codes.cards.new_user.fields.name.description",this.hass.language)}
this.data={...this.data,name:e.target.value}}
>
${this.item?q`
${gi("panels.codes.cards.edit_user.fields.old_code.heading",this.hass.language)}
${gi("panels.codes.cards.edit_user.fields.old_code.description",this.hass.language)}
this.data={...this.data,old_code:String(e.target.value).trim()}}
>
`:""}
${this.item&&!(null===(e=this.data.old_code)||void 0===e?void 0:e.length)?"":q`
${gi("panels.codes.cards.new_user.fields.code.heading",this.hass.language)}
${gi("panels.codes.cards.new_user.fields.code.description",this.hass.language)}
this.data={...this.data,code:String(e.target.value).trim()}}
>
${gi("panels.codes.cards.new_user.fields.confirm_code.heading",this.hass.language)}
${gi("panels.codes.cards.new_user.fields.confirm_code.description",this.hass.language)}
this.repeatCode=String(e.target.value).trim()}
>
`}
${gi("panels.codes.cards.new_user.fields.can_arm.heading",this.hass.language)}
${gi("panels.codes.cards.new_user.fields.can_arm.description",this.hass.language)}
this.data={...this.data,can_arm:e.target.checked}}
>
${gi("panels.codes.cards.new_user.fields.can_disarm.heading",this.hass.language)}
${gi("panels.codes.cards.new_user.fields.can_disarm.description",this.hass.language)}
this.data={...this.data,can_disarm:e.target.checked}}
>
${this.getAreaOptions().length>=2?q`
${gi("panels.codes.cards.new_user.fields.area_limit.heading",this.hass.language)}
${gi("panels.codes.cards.new_user.fields.area_limit.description",this.hass.language)}
${this.getAreaOptions().map(e=>{var t;const a=(this.data.area_limit||[]).includes(e.value)||!(null===(t=this.data.area_limit)||void 0===t?void 0:t.length);return q`
this.toggleSelectArea(e.value,t.target.checked)}
?disabled=${a&&(this.data.area_limit||[]).length<=1}
?checked=${a}
>
this.toggleSelectArea(e.value,!a)}>
${e.name}
`})}
`:""}
${gi("panels.codes.cards.new_user.fields.is_override_code.heading",this.hass.language)}
${gi("panels.codes.cards.new_user.fields.is_override_code.description",this.hass.language)}
this.data={...this.data,is_override_code:e.target.checked}}
>
${this.hass.localize("ui.common.save")}
${this.item?q`
${this.hass.localize("ui.common.delete")}
`:""}
`:q``}getAreaOptions(){let e=Object.keys(this.areas||{}).map(e=>Object({value:e,name:this.areas[e].name}));return e.sort(Pi),e}toggleSelectArea(e,t){if((this.data.area_limit||[]).length<=1&&!t)return;let a=this.data.area_limit||[];a=t?a.includes(e)?a:[...a,e]:a.includes(e)?a.filter(t=>t!=e):a,this.data=Object.assign(Object.assign({},this.data),{area_limit:a})}deleteClick(e){var t,a;this.item&&(t=this.hass,a=this.item,t.callApi("POST","alarmo/users",{user_id:a,remove:!0})).catch(t=>Di(t,e)).then(()=>{this.cancelClick()})}saveClick(e){var t,a,i;let s=Object.assign({},this.data);(null===(t=s.name)||void 0===t?void 0:t.length)?(null===(a=s.code)||void 0===a?void 0:a.length)&&!(s.code.length<4)||this.item&&!(null===(i=s.old_code)||void 0===i?void 0:i.length)?(s.code||"").length&&s.code!==this.repeatCode?(Ni(e,gi("panels.codes.cards.new_user.errors.code_mismatch",this.hass.language)),this.data=Si(this.data,"code"),this.repeatCode=""):(this.item&&(s.old_code||"").length<4&&Si(s,"old_code","code"),this.getAreaOptions().length&&!this.getAreaOptions().every(e=>(this.data.area_limit||[]).includes(e.value))||(s=Object.assign(Object.assign({},s),{area_limit:[]})),Qe(this.hass,s).catch(t=>Di(t,e)).then(()=>{this.cancelClick()})):Ni(e,gi("panels.codes.cards.new_user.errors.no_code",this.hass.language)):Ni(e,gi("panels.codes.cards.new_user.errors.no_name",this.hass.language))}cancelClick(){Pe(0,Ui("codes"),!0)}static get styles(){return o`
${qi}
div.checkbox-list {
display: flex;
flex-direction: row;
}
div.checkbox-list div {
display: flex;
align-items: center;
}
div.checkbox-list div span {
cursor: pointer;
}
`}};t([le()],Ps.prototype,"hass",void 0),t([le()],Ps.prototype,"narrow",void 0),t([le()],Ps.prototype,"item",void 0),t([le()],Ps.prototype,"data",void 0),t([le()],Ps.prototype,"repeatCode",void 0),Ps=t([re("user-editor-card")],Ps);let qs=class extends(et(ne)){constructor(){super(...arguments),this.users={}}hassSubscribe(){return this._fetchData(),[this.hass.connection.subscribeMessage(()=>this._fetchData(),{type:"alarmo_config_updated"})]}async _fetchData(){if(!this.hass)return;const e=await Ge(this.hass);this.data=ji(e,["code_arm_required","code_disarm_required","code_format"]);const t=await Ve(this.hass);this.users=t}render(){return this.hass&&this.data?"new_user"==this.path.subpage?q`
`:this.path.params.edit_user?q`
`:q`
${gi("panels.codes.cards.codes.description",this.hass.language)}
${gi("panels.codes.cards.codes.fields.code_arm_required.heading",this.hass.language)}
${gi("panels.codes.cards.codes.fields.code_arm_required.description",this.hass.language)}
{this.saveData({code_arm_required:e.target.checked})}}
>
${gi("panels.codes.cards.codes.fields.code_disarm_required.heading",this.hass.language)}
${gi("panels.codes.cards.codes.fields.code_disarm_required.description",this.hass.language)}
{this.saveData({code_disarm_required:e.target.checked})}}
>
${gi("panels.codes.cards.codes.fields.code_format.heading",this.hass.language)}
${gi("panels.codes.cards.codes.fields.code_format.description",this.hass.language)}
{this.saveData({code_format:"number"})}}
?disabled=${!this.data.code_arm_required&&!this.data.code_disarm_required}
>
${gi("panels.codes.cards.codes.fields.code_format.code_format_number",this.hass.language)}
{this.saveData({code_format:"text"})}}
?disabled=${!this.data.code_arm_required&&!this.data.code_disarm_required}
>
${gi("panels.codes.cards.codes.fields.code_format.code_format_text",this.hass.language)}
${this.usersPanel()}
`:q``}usersPanel(){if(!this.hass)return q``;const e=Object.values(this.users);e.sort(Pi);const t={icon:{width:"40px"},name:{title:this.hass.localize("ui.components.area-picker.add_dialog.name"),width:"40%",grow:!0,text:!0},code_format:{title:gi("panels.codes.cards.codes.fields.code_format.heading",this.hass.language),width:"40%",hide:this.narrow,text:!0},enabled:{title:gi("common.enabled",this.hass.language),width:"68px",align:"center"}},a=e.map(e=>({id:e.user_id,icon:q`
`,name:xi(e.name),code_format:"number"==e.code_format?xi(gi("panels.codes.cards.codes.fields.code_format.code_format_number",this.hass.language)):"text"==e.code_format?xi(gi("panels.codes.cards.codes.fields.code_format.code_format_text",this.hass.language)):this.hass.localize("state.default.unknown"),enabled:q`
{e.stopPropagation()}}
?checked=${e.enabled}
@change=${t=>this.toggleEnabled(t,e.user_id)}
>
`}));return q`
${gi("panels.codes.cards.user_management.description",this.hass.language)}
{const t=String(e.detail.id);Pe(0,Ui("codes",{params:{edit_user:t}}),!0)}}
>
${gi("panels.codes.cards.user_management.no_items",this.hass.language)}
${gi("panels.codes.cards.user_management.actions.new_user",this.hass.language)}
`}addUserClick(){Pe(0,Ui("codes","new_user"),!0)}saveData(e){this.hass&&(this.data=Object.assign(Object.assign({},this.data),e),Be(this.hass,this.data).catch(e=>Di(e,this.shadowRoot.querySelector("ha-card"))).then())}toggleEnabled(e,t){const a=e.target.checked;Qe(this.hass,{user_id:t,enabled:a}).catch(t=>Di(t,e)).then()}};qs.styles=qi,t([le()],qs.prototype,"hass",void 0),t([le()],qs.prototype,"narrow",void 0),t([le()],qs.prototype,"path",void 0),t([le()],qs.prototype,"data",void 0),t([le()],qs.prototype,"users",void 0),qs=t([re("alarm-view-codes")],qs);const Rs=(e,t)=>{switch(e){case ki.ArmedAway:return{value:ki.ArmedAway,name:gi("common.modes_short.armed_away",t.language),icon:vi.ArmedAway};case ki.ArmedHome:return{value:ki.ArmedHome,name:gi("common.modes_short.armed_home",t.language),icon:vi.ArmedHome};case ki.ArmedNight:return{value:ki.ArmedNight,name:gi("common.modes_short.armed_night",t.language),icon:vi.ArmedNight};case ki.ArmedCustom:return{value:ki.ArmedCustom,name:gi("common.modes_short.armed_custom_bypass",t.language),icon:vi.ArmedCustom};case ki.ArmedVacation:return{value:ki.ArmedVacation,name:gi("common.modes_short.armed_vacation",t.language),icon:vi.ArmedVacation}}},Is=(e,t)=>{switch(e){case $i.Armed:return{value:$i.Armed,name:gi("panels.actions.cards.new_notification.fields.event.choose.armed.name",t.language),description:gi("panels.actions.cards.new_notification.fields.event.choose.armed.description",t.language),icon:"hass:shield-check-outline"};case $i.Disarmed:return{value:$i.Disarmed,name:gi("panels.actions.cards.new_notification.fields.event.choose.disarmed.name",t.language),description:gi("panels.actions.cards.new_notification.fields.event.choose.disarmed.description",t.language),icon:"hass:shield-off-outline"};case $i.Triggered:return{value:$i.Triggered,name:gi("panels.actions.cards.new_notification.fields.event.choose.triggered.name",t.language),description:gi("panels.actions.cards.new_notification.fields.event.choose.triggered.description",t.language),icon:"hass:bell-alert-outline"};case $i.Untriggered:return{value:$i.Untriggered,name:gi("panels.actions.cards.new_notification.fields.event.choose.untriggered.name",t.language),description:gi("panels.actions.cards.new_notification.fields.event.choose.untriggered.description",t.language),icon:"hass:bell-off-outline"};case $i.ArmFailure:return{value:$i.ArmFailure,name:gi("panels.actions.cards.new_notification.fields.event.choose.arm_failure.name",t.language),description:gi("panels.actions.cards.new_notification.fields.event.choose.arm_failure.description",t.language),icon:"hass:alert-outline"};case $i.Arming:return{value:$i.Arming,name:gi("panels.actions.cards.new_notification.fields.event.choose.arming.name",t.language),description:gi("panels.actions.cards.new_notification.fields.event.choose.arming.description",t.language),icon:"hass:home-export-outline"};case $i.Pending:return{value:$i.Pending,name:gi("panels.actions.cards.new_notification.fields.event.choose.pending.name",t.language),description:gi("panels.actions.cards.new_notification.fields.event.choose.pending.description",t.language),icon:"hass:home-import-outline"}}},Us=(e,t,a)=>0==e?{name:a.master.name,value:0}:Object.keys(t).includes(String(e))?{name:t[e].name,value:e}:{name:String(e),value:e},Gs=(e,...t)=>{const a=t.map(t=>{if(!t)return null;const a=Ce(t),i=Me(t);let s={value:t,name:i.replace(/_/g," ").split(" ").map(e=>e.substring(0,1).toUpperCase()+e.substring(1)).join(" "),icon:"hass:home",description:t};switch(a){case"notify":const t=e.states["device_tracker."+i.replace("mobile_app_","")];s=t?Object.assign(Object.assign({},s),{name:t.attributes.friendly_name||Me(t.entity_id),icon:t.attributes.icon||"hass:cellphone-text"}):Object.assign(Object.assign({},s),{icon:"hass:comment-alert"});break;case"tts":s=Object.assign(Object.assign({},s),{icon:"hass:microphone"})}return s}).filter(Ci);return a.sort((e,t)=>{const a=Ce(e.value),i=Ce(t.value);return a!=i?Pi(a,i):Pi(e,t)}),a},Fs=(e,t)=>{let a=[];const i=Object.keys(e).filter(t=>Object.values(e[t].modes).some(e=>e.enabled));return t.master.enabled&&i.length>1&&(a=[...a,0]),a=[...a,...i],a},Vs=(e,t)=>{const a=e=>Object.keys(e.modes).filter(t=>e.modes[t].enabled);if(Ci(e)&&Object.keys(t).includes(String(e)))return a(t[e]);{const e=Object.keys(t).map(e=>a(t[e]));return e[0].filter(t=>e.every(e=>e.includes(t)))}},Hs=(e,t)=>e.map(e=>({value:e,name:e in t.states?t.states[e].attributes.friendly_name||Me(e):e,icon:e in t.states?t.states[e].attributes.icon||ze(Ce(e)):void 0,description:e})),Ys=e=>{let t=[];return"notify"in e.services&&(t=[...t,...Object.keys(e.services.notify).map(e=>"notify."+e)]),"tts"in e.services&&(t=[...t,...Object.keys(e.services.tts).filter(e=>"clear_cache"!=e).map(e=>"tts."+e)]),t},Bs=(...e)=>{if(!e.length||!e.every(e=>e.length))return[];if(1==e.length&&e[0].length>1&&Ei(e[0].map(Ce)).length>1)return Bs(...e[0].map(e=>Array(e)));let t=[...e[0]];return e.forEach(e=>{t=t.map(t=>e.includes(t)?t:"script"==Ce(t)&&e.map(Ce).includes("script")?"script.script":e.map(Me).includes(Me(t))?"homeassistant."+Me(t):null).filter(Ci)}),t},Ks=(e,t,a=1)=>{if(a>10)return[];if(Array.isArray(e)){const i=e.map(e=>Ks(e,t,a+1));return Bs(...i)}if(!Ci(e))return[];const i=Ce(e);switch(i){case"light":case"switch":case"input_boolean":case"siren":return[i+".turn_on",i+".turn_off"];case"script":return[e];case"lock":return["lock.lock","lock.unlock"];case"group":const s=e in t.states?t.states[e]:void 0,n=(null==s?void 0:s.attributes.entity_id)||[];return Ks(n,t,a+1);default:return[]}},Qs=(e,t)=>{let a=[...Object.keys(e.states).filter(t=>Ks(t,e).length)];return t&&t.length&&(a=[...a,...t.filter(e=>!a.includes(e))]),a.sort(Pi),a},Ws=e=>{let t=[...Object.keys(e.states).filter(e=>"media_player"==Ce(e))];return t.sort(Pi),t},Xs=e=>{let t=[{value:"{{arm_mode}}",name:e.translationMetadata.translations.en.nativeName}];return"en"!=e.language&&(t=[...t,{value:`{{arm_mode|lang=${e.language}}}`,name:e.translationMetadata.translations[e.language].nativeName}]),t},Zs=e=>"string"==typeof e&&e.trim().length,Js=(e,t)=>Zs(e)&&t.services[Ce(e)]&&t.services[Ce(e)][Me(e)],en=(e,t)=>Zs(e)&&t.states[e],tn=e=>"object"==typeof e&&null!==e&&!Array.isArray(e),an=e=>"string"==typeof e;let sn=class extends ne{constructor(){super(...arguments),this.items=[],this.value=[],this.label="",this.invalid=!1}shouldUpdate(e){return e.get("items")&&(Mi(this.items,e.get("items"))||this.firstUpdated()),!0}firstUpdated(){this.value.some(e=>!this.items.map(e=>e.value).includes(e))&&(this.value=this.value.filter(e=>this.items.map(e=>e.value).includes(e)),De(this,"value-changed",{value:this.value}))}render(){return q`
${this.value.length?this.value.map(e=>this.items.find(t=>t.value==e)).filter(Ci).map(e=>q`
${e.name}
this._removeClick(e.value)}>
`):""}
!this.value.includes(e.value))}
?disabled=${this.value.length==this.items.length}
label=${this.label}
icons=${!0}
@value-changed=${this._addClick}
?invalid=${this.invalid&&this.value.length!=this.items.length}
>
`}_removeClick(e){this.value=this.value.filter(t=>t!==e),De(this,"value-changed",{value:this.value})}_addClick(e){e.stopPropagation();const t=e.target,a=t.value;this.value.includes(a)||(this.value=[...this.value,a]),t.value="",De(this,"value-changed",{value:[...this.value]})}static get styles(){return o`
div.chip-set {
margin: 0px -4px;
}
div.chip {
height: 32px;
border-radius: 16px;
border: 2px solid rgb(168, 232, 251);
line-height: 1.25rem;
font-size: 0.875rem;
font-weight: 400;
padding: 0px 12px;
display: inline-flex;
align-items: center;
box-sizing: border-box;
margin: 4px;
}
.icon {
vertical-align: middle;
outline: none;
display: flex;
align-items: center;
border-radius: 50%;
padding: 6px;
color: rgba(0, 0, 0, 0.54);
background: rgb(168, 232, 251);
--mdc-icon-size: 20px;
margin-left: -14px !important;
}
.label {
margin: 0px 4px;
}
.button {
cursor: pointer;
background: var(--secondary-text-color);
border-radius: 50%;
--mdc-icon-size: 14px;
color: var(--card-background-color);
width: 16px;
height: 16px;
padding: 1px;
box-sizing: border-box;
display: inline-flex;
align-items: center;
margin-right: -6px !important;
}
`}};var nn;t([le()],sn.prototype,"hass",void 0),t([le()],sn.prototype,"items",void 0),t([le({type:Array})],sn.prototype,"value",void 0),t([le()],sn.prototype,"label",void 0),t([le({type:Boolean})],sn.prototype,"invalid",void 0),sn=t([re("alarmo-selector")],sn),function(e){e[e.Yaml=0]="Yaml",e[e.UI=1]="UI"}(nn||(nn={}));let rn=class extends ne{constructor(){super(...arguments),this.config={type:wi.Notification,triggers:[{}],actions:[{}]},this.viewMode=nn.UI,this.errors={}}async firstUpdated(){if(await Ue(),this.areas=await Ze(this.hass),this.alarmoConfig=await Ge(this.hass),this.item){let e=this.item.actions.map(e=>Si(e,"entity_id"));this.config=Object.assign(Object.assign({},this.item),{actions:[e[0],...e.slice(1)]}),this.config.triggers.length>1&&(this.config=Object.assign(Object.assign({},this.config),{triggers:[this.config.triggers[0]]}));let t=this.config.triggers[0].area;Ci(t)&&!Fs(this.areas,this.alarmoConfig).includes(t)?t=void 0:null===t&&(t=0),this._setArea(new CustomEvent("value-changed",{detail:{value:t}}))}if(!Ci(this.config.triggers[0].area)){const e=Fs(this.areas,this.alarmoConfig);1==e.length?this._setArea(new CustomEvent("value-changed",{detail:{value:e[0]}})):e.includes(0)&&this._setArea(new CustomEvent("value-changed",{detail:{value:0}}))}}render(){var e,t,a,i;return this.hass&&this.areas&&this.alarmoConfig?q`
${gi("panels.actions.cards.new_notification.description",this.hass.language)}
${gi("panels.actions.cards.new_notification.fields.event.heading",this.hass.language)}
${gi("panels.actions.cards.new_notification.fields.event.description",this.hass.language)}
Is(e,this.hass))}
label=${gi("panels.actions.cards.new_action.fields.event.heading",this.hass.language)}
icons=${!0}
.value=${this.config.triggers[0].event}
@value-changed=${this._setEvent}
?invalid=${this.errors.event}
>
${Object.keys(this.areas).length>1?q`
${gi("panels.actions.cards.new_action.fields.area.heading",this.hass.language)}
${gi("panels.actions.cards.new_action.fields.area.description",this.hass.language)}
Us(e,this.areas,this.alarmoConfig))}
clearable=${!0}
label=${gi("panels.actions.cards.new_action.fields.area.heading",this.hass.language)}
.value=${this.config.triggers[0].area}
@value-changed=${this._setArea}
?invalid=${this.errors.area||!this.config.triggers[0].area&&!this.alarmoConfig.master.enabled}
>
`:""}
${gi("panels.actions.cards.new_notification.fields.mode.heading",this.hass.language)}
${gi("panels.actions.cards.new_notification.fields.mode.description",this.hass.language)}
Rs(e,this.hass))}
label=${gi("panels.actions.cards.new_action.fields.mode.heading",this.hass.language)}
.value=${this.config.triggers[0].modes||[]}
@value-changed=${this._setModes}
?invalid=${this.errors.modes}
>
${this.viewMode==nn.UI?q`
${gi("panels.actions.cards.new_notification.fields.target.heading",this.hass.language)}
${gi("panels.actions.cards.new_notification.fields.target.description",this.hass.language)}
${this.config.actions[0].service&&"notify"!=Ce(this.config.actions[0].service)?"":q`
${gi("panels.actions.cards.new_notification.fields.title.heading",this.hass.language)}
${gi("panels.actions.cards.new_notification.fields.title.description",this.hass.language)}
`}
${this.config.actions[0].service&&"tts"==Ce(this.config.actions[0].service)?q`
${gi("panels.actions.cards.new_action.fields.entity.heading",this.hass.language)}
${gi("panels.actions.cards.new_action.fields.entity.description",this.hass.language)}
`:""}
${gi("panels.actions.cards.new_notification.fields.message.heading",this.hass.language)}
${gi("panels.actions.cards.new_notification.fields.message.description",this.hass.language)}
this._setMessage(e.target.value)}
?invalid=${this.errors.message}
>
${this.config.triggers[0].event?q`
${gi("panels.actions.cards.new_notification.fields.message.insert_wildcard",this.hass.language)}:
{let a=[];return a=[],e&&![$i.Pending,$i.Triggered,$i.ArmFailure].includes(e)||(a=[...a,{name:"Open Sensors",value:"{{open_sensors}}"}]),e&&![$i.Armed].includes(e)||(a=[...a,{name:"Bypassed Sensors",value:"{{bypassed_sensors}}"}]),(!e||(null==t?void 0:t.code_arm_required)&&[$i.Armed,$i.Arming,$i.ArmFailure].includes(e)||(null==t?void 0:t.code_disarm_required)&&[$i.Disarmed,$i.Untriggered].includes(e))&&(a=[...a,{name:"Changed By",value:"{{changed_by}}"}]),e&&![$i.Armed,$i.Arming,$i.Pending,$i.Triggered,$i.ArmFailure].includes(e)||(a=[...a,{name:"Arm Mode",value:"{{arm_mode}}"}]),a})(this.config.triggers[0].event,this.alarmoConfig)}
@value-changed=${e=>this._insertWildCard(e.detail)}
>
`:""}
${null!==this._getOpenSensorsFormat()?q`
${gi("panels.actions.cards.new_notification.fields.open_sensors_format.heading",this.hass.language)}
${gi("panels.actions.cards.new_notification.fields.open_sensors_format.description",this.hass.language)}
{let t=[];return t="en"!=e.language?[...t,{value:"{{open_sensors}}",name:`${gi("panels.actions.cards.new_notification.fields.open_sensors_format.options.default",e.language)} (${e.translationMetadata.translations.en.nativeName})`},{value:`{{open_sensors|lang=${e.language}}}`,name:`${gi("panels.actions.cards.new_notification.fields.open_sensors_format.options.default",e.language)} (${e.translationMetadata.translations[e.language].nativeName})`}]:[...t,{value:"{{open_sensors}}",name:gi("panels.actions.cards.new_notification.fields.open_sensors_format.options.default",e.language)}],t=[...t,{value:"{{open_sensors|format=short}}",name:gi("panels.actions.cards.new_notification.fields.open_sensors_format.options.short",e.language)}],t})(this.hass)}
.value=${this._getOpenSensorsFormat(!0)}
@value-changed=${this._setOpenSensorsFormat}
>
`:""}
${null!==this._getArmModeFormat()&&(Xs(this.hass).length>1||1==Xs(this.hass).length&&Xs(this.hass)[0].value!=this._getArmModeFormat())?q`
${gi("panels.actions.cards.new_notification.fields.arm_mode_format.heading",this.hass.language)}
${gi("panels.actions.cards.new_notification.fields.arm_mode_format.description",this.hass.language)}
`:""}
`:q`
${gi("components.editor.edit_in_yaml",this.hass.language)}
${this.errors.service||this.errors.title||this.errors.message?q`
${this.hass.localize("ui.errors.config.key_missing","key",Object.entries(this.errors).find(([e,t])=>t&&["service","title","message","entity"].includes(e))[0])}
`:""}
`}
${this.viewMode==nn.Yaml?gi("components.editor.ui_mode",this.hass.language):gi("components.editor.yaml_mode",this.hass.language)}
${gi("panels.actions.cards.new_notification.actions.test",this.hass.language)}
${gi("panels.actions.cards.new_notification.fields.name.heading",this.hass.language)}
${gi("panels.actions.cards.new_notification.fields.name.description",this.hass.language)}
${(null===(i=this.item)||void 0===i?void 0:i.automation_id)?q`
${gi("panels.actions.cards.new_notification.fields.delete.heading",this.hass.language)}
${gi("panels.actions.cards.new_notification.fields.delete.description",this.hass.language)}
${this.hass.localize("ui.common.delete")}
`:""}
${this.hass.localize("ui.common.save")}
`:q``}_setEvent(e){e.stopPropagation();const t=e.detail.value;let a=this.config.triggers;Object.assign(a,{0:Object.assign(Object.assign({},a[0]),{event:t})}),this.config=Object.assign(Object.assign({},this.config),{triggers:a}),Object.keys(this.errors).includes("event")&&this._validateConfig()}_setArea(e){var t;e.stopPropagation();const a=e.detail.value;let i=this.config.triggers;Object.assign(i,{0:Object.assign(Object.assign({},i[0]),{area:a})});const s=Vs(a,this.areas);(null===(t=i[0].modes)||void 0===t?void 0:t.length)&&this._setModes(new CustomEvent("value-changed",{detail:{value:i[0].modes.filter(e=>s.includes(e))}})),this.config=Object.assign(Object.assign({},this.config),{triggers:i}),Object.keys(this.errors).includes("area")&&this._validateConfig()}_setModes(e){e.stopPropagation();const t=e.detail.value;let a=this.config.triggers;Object.assign(a,{0:Object.assign(Object.assign({},a[0]),{modes:t})}),this.config=Object.assign(Object.assign({},this.config),{triggers:a}),Object.keys(this.errors).includes("modes")&&this._validateConfig()}_setService(e){e.stopPropagation();const t=String(e.detail.value);let a=this.config.actions;Object.assign(a,{0:Object.assign(Object.assign(Object.assign({},a[0]),{service:t}),Si(a[0],"service"))}),(a[0].data||{}).entity_id&&"notify"==Ce(t)&&Object.assign(a,{0:Object.assign(Object.assign({},a[0]),{data:Si(a[0].data||{},"entity_id")})}),this.config=Object.assign(Object.assign({},this.config),{actions:a}),Object.keys(this.errors).includes("service")&&this._validateConfig()}_setTitle(e){e.stopPropagation();const t=e.target.value;let a=this.config.actions;Object.assign(a,{0:Object.assign(Object.assign({},a[0]),{service:a[0].service||"",data:Object.assign(Object.assign({},a[0].data||{}),{title:t})})}),this.config=Object.assign(Object.assign({},this.config),{actions:a}),Object.keys(this.errors).includes("title")&&this._validateConfig()}_setEntity(e){e.stopPropagation();const t=e.target.value;let a=this.config.actions;Object.assign(a,{0:Object.assign(Object.assign({},a[0]),{service:a[0].service||"",data:Object.assign(Object.assign({},a[0].data||{}),{entity_id:t})})}),this.config=Object.assign(Object.assign({},this.config),{actions:a}),Object.keys(this.errors).includes("entity")&&this._validateConfig()}_setMessage(e){let t=this.config.actions;Object.assign(t,{0:Object.assign(Object.assign({},t[0]),{service:t[0].service||"",data:Object.assign(Object.assign({},t[0].data||{}),{message:e})})}),this.config=Object.assign(Object.assign({},this.config),{actions:t}),Object.keys(this.errors).includes("message")&&this._validateConfig()}_setName(e){e.stopPropagation();const t=e.target.value;this.config=Object.assign(Object.assign({},this.config),{name:t})}_setYaml(e){const t=e.detail.value;let a={};an(null==t?void 0:t.service)&&(a=Object.assign(Object.assign({},a),{service:String(t.service)})),tn(null==t?void 0:t.data)&&(a=Object.assign(Object.assign({},a),{data:t.data})),Object.keys(a).length&&(this.config=Object.assign(Object.assign({},this.config),{actions:Object.assign(this.config.actions,{0:Object.assign(Object.assign({},this.config.actions[0]),a)})})),Object.keys(this.errors).some(e=>["service","message","title"].includes(e))&&this._validateConfig()}_validateConfig(){var e;this.errors={};const t=this._parseAutomation(),a=t.triggers[0];a.event&&Object.values($i).includes(a.event)||(this.errors=Object.assign(Object.assign({},this.errors),{event:!0})),Ci(a.area)&&Fs(this.areas,this.alarmoConfig).includes(a.area)||(this.errors=Object.assign(Object.assign({},this.errors),{area:!0})),(a.modes||[]).every(e=>Vs(a.area,this.areas).includes(e))||(this.errors=Object.assign(Object.assign({},this.errors),{modes:!0}));const i=t.actions[0];return!i.service||!Ys(this.hass).includes(i.service)&&"script"!=Ce(i.service)?this.errors=Object.assign(Object.assign({},this.errors),{service:!0}):!i.service||"tts"!=Ce(i.service)||Object.keys(i.data||{}).includes("entity_id")&&Ws(this.hass).includes(i.data.entity_id)||(this.errors=Object.assign(Object.assign({},this.errors),{entity:!0})),Zs(null===(e=i.data)||void 0===e?void 0:e.message)||(this.errors=Object.assign(Object.assign({},this.errors),{message:!0})),Zs(t.name)||(this.errors=Object.assign(Object.assign({},this.errors),{name:!0})),!Object.values(this.errors).length}_validAction(){var e;const t=this._parseAutomation().actions[0];return t.service&&("script"==Ce(t.service)||Ys(this.hass).includes(t.service))&&Zs(null===(e=t.data)||void 0===e?void 0:e.message)}_insertWildCard(e){var t;const a=this.shadowRoot.querySelector("#message");a&&a.focus();let i=(null===(t=this.config.actions[0].data)||void 0===t?void 0:t.message)||"";i=a&&null!==a.selectionStart&&null!==a.selectionEnd?i.substring(0,a.selectionStart)+e+i.substring(a.selectionEnd,i.length):i+e,this._setMessage(i)}_toggleYamlMode(){if(this.viewMode=this.viewMode==nn.UI?nn.Yaml:nn.UI,this.viewMode==nn.Yaml){let e=Object.assign({},this.config.actions[0]),t="object"==typeof e.data&&Ci(e.data)?e.data:{};e=Object.assign(Object.assign({},e),{service:e.service||""}),t.message||(t=Object.assign(Object.assign({},t),{message:""})),Ys(this.hass).includes(e.service)&&("notify"!=Ce(e.service)||t.title||(t=Object.assign(Object.assign({},t),{title:""})),"tts"!=Ce(e.service)||t.entity_id||(t=Object.assign(Object.assign({},t),{entity_id:""}))),e=Object.assign(Object.assign({},e),{data:t}),this.config=Object.assign(Object.assign({},this.config),{actions:Object.assign(this.config.actions,{0:e})})}}_namePlaceholder(){const e=this.config.triggers[0].event,t=this.config.actions[0].service?Ce(this.config.actions[0].service):null;if(!e)return"";if("notify"==t){const t=Gs(this.hass,this.config.actions[0].service);return t.length?gi("panels.actions.cards.new_notification.fields.name.placeholders."+e,this.hass.language,"{target}",t[0].name):""}if("tts"==t){const t="object"==typeof this.config.actions[0].data&&Ci(this.config.actions[0].data)?this.config.actions[0].data.entity_id:null;if(!t||!this.hass.states[t])return"";const a=Oi(this.hass.states[t]);return gi("panels.actions.cards.new_notification.fields.name.placeholders."+e,this.hass.language,"{target}",a)}return""}_messagePlaceholder(){const e=this.config.triggers[0].event;return e?gi("panels.actions.cards.new_notification.fields.message.placeholders."+e,this.hass.language):""}_parseAutomation(){var e;let t=Object.assign({},this.config),a=t.actions[0];return!Zs(null===(e=a.data)||void 0===e?void 0:e.message)&&this.viewMode==nn.UI&&this._messagePlaceholder()&&(a=Object.assign(Object.assign({},a),{data:Object.assign(Object.assign({},a.data),{message:this._messagePlaceholder()})}),Object.assign(t,{actions:Object.assign(t.actions,{0:a})})),!Zs(t.name)&&this._namePlaceholder()&&(t=Object.assign(Object.assign({},t),{name:this._namePlaceholder()})),t}_getOpenSensorsFormat(e=!1){var t;const a=((null===(t=this.config.actions[0].data)||void 0===t?void 0:t.message)||"").match(/{{open_sensors(\|[^}]+)?}}/);return null!==a?a[0]:e?"{{open_sensors}}":null}_setOpenSensorsFormat(e){var t;e.stopPropagation();const a=String(e.detail.value);let i=(null===(t=this.config.actions[0].data)||void 0===t?void 0:t.message)||"";i=i.replace(/{{open_sensors(\|[^}]+)?}}/,a);let s=this.config.actions;Object.assign(s,{0:Object.assign(Object.assign({},s[0]),{service:s[0].service||"",data:Object.assign(Object.assign({},s[0].data||{}),{message:i})})}),this.config=Object.assign(Object.assign({},this.config),{actions:s})}_getArmModeFormat(e=!1){var t;const a=((null===(t=this.config.actions[0].data)||void 0===t?void 0:t.message)||"").match(/{{arm_mode(\|[^}]+)?}}/);return null!==a?a[0]:e?"{{arm_mode}}":null}_setArmModeFormat(e){var t;e.stopPropagation();const a=String(e.detail.value);let i=(null===(t=this.config.actions[0].data)||void 0===t?void 0:t.message)||"";i=i.replace(/{{arm_mode(\|[^}]+)?}}/,a);let s=this.config.actions;Object.assign(s,{0:Object.assign(Object.assign({},s[0]),{service:s[0].service||"",data:Object.assign(Object.assign({},s[0].data||{}),{message:i})})}),this.config=Object.assign(Object.assign({},this.config),{actions:s})}_saveClick(e){if(!this._validateConfig())return;let t=this._parseAutomation();Vs(t.triggers[0].area,this.areas).every(e=>{var a;return null===(a=t.triggers[0].modes)||void 0===a?void 0:a.includes(e)})&&(t=Object.assign(Object.assign({},t),{triggers:Object.assign(t.triggers,{0:Object.assign(Object.assign({},t.triggers[0]),{modes:[]})})})),this.item&&(t=Object.assign(Object.assign({},t),{automation_id:this.item.automation_id})),We(this.hass,t).catch(t=>Di(t,e)).then(()=>this._cancelClick())}_deleteClick(e){var t;(null===(t=this.item)||void 0===t?void 0:t.automation_id)&&Xe(this.hass,this.item.automation_id).catch(t=>Di(t,e)).then(()=>this._cancelClick())}_testClick(e){const t=this._parseAutomation().actions[0],[a,i]=t.service.split(".");let s=t.data.message;s=s.replace("{{open_sensors|format=short}}","Some Example Sensor"),s=s.replace(/{{open_sensors(\|[^}]+)?}}/,"Some Example Sensor is open"),s=s.replace("{{bypassed_sensors}}","Some Bypassed Sensor"),s=s.replace(/{{arm_mode(\|[^}]+)?}}/,"Armed away"),s=s.replace("{{changed_by}}","Some Example User"),this.hass.callService(a,i,Object.assign(Object.assign({},t.data),{message:s})).then().catch(t=>{Ni(e,t.message)})}_cancelClick(){Pe(0,Ui("actions"),!0)}static get styles(){return o`
div.content {
padding: 28px 20px 0;
max-width: 1040px;
margin: 0 auto;
display: flex;
flex-direction: column;
}
div.header {
font-size: 24px;
font-weight: 400;
letter-spacing: -0.012em;
line-height: 32px;
opacity: var(--dark-primary-opacity);
}
div.section-header {
font-size: 18px;
font-weight: 400;
letter-spacing: -0.012em;
line-height: 32px;
opacity: var(--dark-primary-opacity);
margin: 20px 0px 5px 10px;
}
div.actions {
padding: 20px 0px 30px 0px;
}
mwc-button ha-icon {
margin-right: 6px;
--mdc-icon-size: 20px;
}
.toggle-button {
position: absolute;
right: 20px;
top: 20px;
}
h2 {
margin-top: 10px;
font-size: 24px;
font-weight: 400;
letter-spacing: -0.012em;
}
span.error-message {
color: var(--error-color);
}
mwc-button.warning {
--mdc-theme-primary: var(--error-color);
}
mwc-button.save-button {
--mdc-theme-primary: rgba(var(--rgb-primary-color), 0.8);
}
div.heading {
display: grid;
grid-template-areas:
'header icon'
'description icon';
grid-template-rows: 1fr 1fr;
grid-template-columns: 1fr 48px;
margin: 20px 0px 10px 10px;
}
div.heading .icon {
grid-area: icon;
}
div.heading .header {
grid-area: header;
}
div.heading .description {
grid-area: description;
}
ha-textarea[invalid] {
--mdc-text-field-idle-line-color: var(--mdc-theme-error);
--mdc-text-field-label-ink-color: var(--mdc-theme-error);
}
`}};var on;t([le({attribute:!1})],rn.prototype,"hass",void 0),t([le()],rn.prototype,"narrow",void 0),t([le()],rn.prototype,"config",void 0),t([le()],rn.prototype,"item",void 0),t([le()],rn.prototype,"areas",void 0),t([le()],rn.prototype,"alarmoConfig",void 0),t([le()],rn.prototype,"viewMode",void 0),t([le()],rn.prototype,"errors",void 0),rn=t([re("notification-editor-card")],rn),function(e){e[e.Yaml=0]="Yaml",e[e.UI=1]="UI"}(on||(on={}));let ln=class extends ne{constructor(){super(...arguments),this.config={type:wi.Action,triggers:[{}],actions:[{}]},this.viewMode=on.UI,this.errors={}}async firstUpdated(){if(await Ue(),this.areas=await Ze(this.hass),this.alarmoConfig=await Ge(this.hass),this.item){let e=this.item.actions.map(e=>e.entity_id?e:Si(e,"entity_id"));this.config=Object.assign(Object.assign({},this.item),{actions:[e[0],...e.slice(1)]}),this.config.triggers.length>1&&(this.config=Object.assign(Object.assign({},this.config),{triggers:[this.config.triggers[0]]}));let t=this.config.triggers[0].area;Ci(t)&&!Fs(this.areas,this.alarmoConfig).includes(t)?t=void 0:null===t&&(t=0),this._setArea(new CustomEvent("value-changed",{detail:{value:t}})),this._hasCustomEntities()&&(this.viewMode=on.Yaml)}if(!Ci(this.config.triggers[0].area)){const e=Fs(this.areas,this.alarmoConfig);1==e.length?this._setArea(new CustomEvent("value-changed",{detail:{value:e[0]}})):e.includes(0)&&this._setArea(new CustomEvent("value-changed",{detail:{value:0}}))}!this.item||this.config.triggers[0].area||this.alarmoConfig.master.enabled||(this.errors=Object.assign(Object.assign({},this.errors),{area:!0}))}render(){var e;return this.hass&&this.areas&&this.alarmoConfig?q`
${gi("panels.actions.cards.new_action.description",this.hass.language)}
${gi("panels.actions.cards.new_action.fields.event.heading",this.hass.language)}
${gi("panels.actions.cards.new_action.fields.event.description",this.hass.language)}
Is(e,this.hass))}
label=${gi("panels.actions.cards.new_action.fields.event.heading",this.hass.language)}
icons=${!0}
.value=${this.config.triggers[0].event}
@value-changed=${this._setEvent}
?invalid=${this.errors.event}
>
${Object.keys(this.areas).length>1?q`
${gi("panels.actions.cards.new_action.fields.area.heading",this.hass.language)}
${gi("panels.actions.cards.new_action.fields.area.description",this.hass.language)}
Us(e,this.areas,this.alarmoConfig))}
clearable=${!0}
label=${gi("panels.actions.cards.new_action.fields.area.heading",this.hass.language)}
.value=${this.config.triggers[0].area}
@value-changed=${this._setArea}
?invalid=${this.errors.area}
>
`:""}
${gi("panels.actions.cards.new_notification.fields.mode.heading",this.hass.language)}
${gi("panels.actions.cards.new_notification.fields.mode.description",this.hass.language)}
Rs(e,this.hass))}
label=${gi("panels.actions.cards.new_action.fields.mode.heading",this.hass.language)}
.value=${this.config.triggers[0].modes||[]}
@value-changed=${this._setModes}
?invalid=${this.errors.modes}
>
${this.viewMode==on.UI?q`
${gi("panels.actions.cards.new_action.fields.entity.heading",this.hass.language)}
${gi("panels.actions.cards.new_action.fields.entity.description",this.hass.language)}
${this._getEntities().length?q`
${gi("panels.actions.cards.new_action.fields.action.heading",this.hass.language)}
${gi("panels.actions.cards.new_action.fields.action.description",this.hass.language)}
${this.renderActions()||gi("panels.actions.cards.new_action.fields.action.no_common_actions",this.hass.language)}
${this.errors.service?q`
${this.hass.localize("ui.common.error_required",this.hass.language)}
`:""}
`:""}
`:q`
${gi("components.editor.edit_in_yaml",this.hass.language)}
${this.errors.service||this.errors.entity_id?q`
${this.hass.localize("ui.errors.config.key_missing","key",Object.entries(this.errors).find(([e,t])=>t&&["service","entity_id"].includes(e))[0])}
`:""}
`}
${this.viewMode==on.Yaml?gi("components.editor.ui_mode",this.hass.language):gi("components.editor.yaml_mode",this.hass.language)}
${gi("panels.actions.cards.new_notification.actions.test",this.hass.language)}
${gi("panels.actions.cards.new_action.fields.name.heading",this.hass.language)}
${gi("panels.actions.cards.new_action.fields.name.description",this.hass.language)}
${(null===(e=this.item)||void 0===e?void 0:e.automation_id)?q`
${gi("panels.actions.cards.new_notification.fields.delete.heading",this.hass.language)}
${gi("panels.actions.cards.new_notification.fields.delete.description",this.hass.language)}
${this.hass.localize("ui.common.delete")}
`:""}
${this.hass.localize("ui.common.save")}
`:q``}renderActions(){let e=this.config.actions.map(e=>e.entity_id),t=Ks(e,this.hass);if(!t.length)return;return t.map(e=>q`
this._setAction(e)}
>
${((e,t)=>{let a=Me(e);switch("script"==Ce(e)&&(a="run"),a){case"turn_on":return t.localize("ui.card.media_player.turn_on");case"turn_off":return t.localize("ui.card.media_player.turn_off");case"lock":return t.localize("ui.card.lock.lock");case"unlock":return t.localize("ui.card.lock.unlock");case"run":return t.localize("ui.card.script.run");default:return a}})(e,this.hass)}
`)}_selectedAction(){let e=this.config.actions.map(e=>e.service);return e.every(Ci)?(e=Ei(Bs(e.filter(Ci))),1==e.length?e[0]:null):null}_setEvent(e){e.stopPropagation();const t=e.detail.value;let a=this.config.triggers;Object.assign(a,{0:Object.assign(Object.assign({},a[0]),{event:t})}),this.config=Object.assign(Object.assign({},this.config),{triggers:a}),Object.keys(this.errors).includes("event")&&this._validateConfig()}_setArea(e){var t;e.stopPropagation();const a=e.detail.value;let i=this.config.triggers;Object.assign(i,{0:Object.assign(Object.assign({},i[0]),{area:a})});const s=Vs(a,this.areas);(null===(t=i[0].modes)||void 0===t?void 0:t.length)&&this._setModes(new CustomEvent("value-changed",{detail:{value:i[0].modes.filter(e=>s.includes(e))}})),this.config=Object.assign(Object.assign({},this.config),{triggers:i}),Object.keys(this.errors).includes("area")&&this._validateConfig()}_setModes(e){e.stopPropagation();const t=e.detail.value,a=this.config.triggers;Object.assign(a,{0:Object.assign(Object.assign({},a[0]),{modes:t})}),this.config=Object.assign(Object.assign({},this.config),{triggers:a}),Object.keys(this.errors).includes("service")&&this._validateConfig()}_setEntity(e){e.stopPropagation();const t=e.detail.value;let a=this.config.actions,i=null;if(t.length>a.length&&this._selectedAction()&&(i=this._selectedAction()),a.length>t.length){let e=a.findIndex(e=>!t.includes(e.entity_id||""));e<0&&(e=a.length-1),a.splice(e,1)}t.length||Object.assign(a,{0:Si(a[0],"entity_id")}),t.forEach((e,t)=>{let i=a.length>t?Object.assign({},a[t]):{};i=i.entity_id==e?Object.assign({},i):{entity_id:e},Object.assign(a,{[t]:i})}),this.config=Object.assign(Object.assign({},this.config),{actions:a}),i&&this._setAction(i),Object.keys(this.errors).includes("entity_id")&&this._validateConfig()}_setAction(e){let t=this.config.actions,a=this.config.actions.map(e=>e.entity_id);Ks(a,this.hass).length&&(t.forEach((a,i)=>{let s=Ks(a.entity_id,this.hass),n=(r=e,s.find(e=>e==r||"turn_on"==Me(r)&&"turn_on"==Me(e)||"turn_off"==Me(r)&&"turn_off"==Me(e)||"script"==Ce(r)&&"script"==Ce(e)));var r;Object.assign(t,{[i]:Object.assign({service:n},Si(a,"service"))})}),this.config=Object.assign(Object.assign({},this.config),{actions:t}),Object.keys(this.errors).includes("service")&&this._validateConfig())}_setName(e){e.stopPropagation();const t=e.target.value;this.config=Object.assign(Object.assign({},this.config),{name:t})}_setYaml(e){let t=e.detail.value,a=[{}];var i;tn(t)&&(t=[t]),"object"==typeof(i=t)&&null!==i&&Array.isArray(i)&&(t.forEach((e,t)=>{let i={};tn(e)&&an(e.service)&&(i=Object.assign(Object.assign({},i),{service:e.service})),tn(e)&&an(e.entity_id)&&(i=Object.assign(Object.assign({},i),{entity_id:e.entity_id})),tn(e)&&tn(e.data)&&(i=Object.assign(Object.assign({},i),{data:e.data})),Object.assign(a,{[t]:i})}),this.config=Object.assign(Object.assign({},this.config),{actions:a}))}_validateConfig(){this.errors={};const e=this._parseAutomation(),t=e.triggers[0];t.event&&Object.values($i).includes(t.event)||(this.errors=Object.assign(Object.assign({},this.errors),{event:!0})),Ci(t.area)&&Fs(this.areas,this.alarmoConfig).includes(t.area)||(this.errors=Object.assign(Object.assign({},this.errors),{area:!0})),(t.modes||[]).every(e=>Vs(t.area,this.areas).includes(e))||(this.errors=Object.assign(Object.assign({},this.errors),{modes:!0}));let a=e.actions.map(e=>e.entity_id);this.viewMode==on.Yaml&&(a=a.filter(Ci)),e.actions.length&&a.every(e=>en(e,this.hass))||(this.errors=Object.assign(Object.assign({},this.errors),{entity_id:!0}));const i=e.actions.map(e=>e.service).filter(Ci);if(!i.length||!i.every(e=>Js(e,this.hass))){this.errors=Object.assign(Object.assign({},this.errors),{service:!0}),!Ks(a,this.hass).length&&i.length&&(this.viewMode=on.Yaml)}return Zs(e.name)||(this.errors=Object.assign(Object.assign({},this.errors),{name:!0})),!Object.values(this.errors).length}_validAction(){const e=this._parseAutomation(),t=e.actions.map(e=>e.service);let a=e.actions.map(e=>e.entity_id);return this.viewMode==on.Yaml&&(a=a.filter(Ci)),t.length&&t.every(e=>Js(e,this.hass))&&a.every(e=>en(e,this.hass))}_toggleYamlMode(){this.viewMode=this.viewMode==on.UI?on.Yaml:on.UI,this.viewMode==on.Yaml&&(this.config=Object.assign(Object.assign({},this.config),{actions:Object.assign(this.config.actions,{0:Object.assign(Object.assign({},this.config.actions[0]),{service:this.config.actions[0].service||"",data:Object.assign({},this.config.actions[0].data||{})})})}))}_namePlaceholder(){var e,t,a,i;if(!this._validAction)return"";const s=this.config.triggers[0].event,n=this.config.actions.map(e=>e.entity_id).filter(Ci),r=Hs(n,this.hass).map(e=>e.name).join(", "),o=Ei(this.config.actions.map(e=>e.service).filter(Ci).map(e=>Me(e)));let l=void 0;return 1==o.length&&(null===(e=o[0])||void 0===e?void 0:e.includes("turn_on"))&&(l=this.hass.localize("state.default.on")),1==o.length&&(null===(t=o[0])||void 0===t?void 0:t.includes("turn_off"))&&(l=this.hass.localize("state.default.off")),1==o.length&&(null===(a=o[0])||void 0===a?void 0:a.includes("lock"))&&(l=this.hass.localize("component.lock.state._.locked")),1==o.length&&(null===(i=o[0])||void 0===i?void 0:i.includes("unlock"))&&(l=this.hass.localize("component.lock.state._.unlocked")),s&&r&&l?gi("panels.actions.cards.new_action.fields.name.placeholders."+s,this.hass.language,"entity",r,"state",l):""}_getEntities(){return Ei(this.config.actions.map(e=>e.entity_id).filter(Ci))||[]}_hasCustomEntities(){return this._getEntities().some(e=>!Qs(this.hass).includes(e))}_parseAutomation(){let e=Object.assign({},this.config);return!Zs(e.name)&&this._namePlaceholder()&&(e=Object.assign(Object.assign({},e),{name:this._namePlaceholder()})),e}_saveClick(e){if(!this._validateConfig())return;let t=this._parseAutomation();Vs(t.triggers[0].area,this.areas).every(e=>{var a;return null===(a=t.triggers[0].modes)||void 0===a?void 0:a.includes(e)})&&(t=Object.assign(Object.assign({},t),{triggers:Object.assign(t.triggers,{0:Object.assign(Object.assign({},t.triggers[0]),{modes:[]})})})),We(this.hass,t).catch(t=>Di(t,e)).then(()=>this._cancelClick())}_deleteClick(e){var t;(null===(t=this.item)||void 0===t?void 0:t.automation_id)&&Xe(this.hass,this.item.automation_id).catch(t=>Di(t,e)).then(()=>this._cancelClick())}_testClick(e){this._parseAutomation().actions.forEach(t=>{const[a,i]=t.service.split(".");let s=Object.assign({},t.data);t.entity_id&&(s=Object.assign(Object.assign({},s),{entity_id:t.entity_id})),this.hass.callService(a,i,s).then().catch(t=>{Ni(e,t.message)})})}_cancelClick(){Pe(0,Ui("actions"),!0)}static get styles(){return o`
div.content {
padding: 28px 20px 0;
max-width: 1040px;
margin: 0 auto;
display: flex;
flex-direction: column;
}
div.header {
font-size: 24px;
font-weight: 400;
letter-spacing: -0.012em;
line-height: 32px;
opacity: var(--dark-primary-opacity);
}
div.section-header {
font-size: 18px;
font-weight: 400;
letter-spacing: -0.012em;
line-height: 32px;
opacity: var(--dark-primary-opacity);
margin: 20px 0px 5px 10px;
}
div.actions {
padding: 20px 0px 30px 0px;
}
mwc-button ha-icon {
margin-right: 6px;
--mdc-icon-size: 20px;
}
.toggle-button {
position: absolute;
right: 20px;
top: 20px;
}
h2 {
margin-top: 10px;
font-size: 24px;
font-weight: 400;
letter-spacing: -0.012em;
}
span.error-message {
color: var(--error-color);
font-size: 0.875rem;
display: flex;
margin-top: 10px;
}
mwc-button.warning {
--mdc-theme-primary: var(--error-color);
}
mwc-button.save-button {
--mdc-theme-primary: rgba(var(--rgb-primary-color), 0.8);
}
mwc-button.active {
background: var(--primary-color);
--mdc-theme-primary: var(--text-primary-color);
border-radius: 4px;
}
mwc-button[disabled].active {
background: var(--disabled-text-color);
--mdc-button-disabled-ink-color: var(--text-primary-color);
}
div.heading {
display: grid;
grid-template-areas:
'header icon'
'description icon';
grid-template-rows: 1fr 1fr;
grid-template-columns: 1fr 48px;
margin: 20px 0px 10px 10px;
}
div.heading .icon {
grid-area: icon;
}
div.heading .header {
grid-area: header;
}
div.heading .description {
grid-area: description;
}
`}};t([le({attribute:!1})],ln.prototype,"hass",void 0),t([le()],ln.prototype,"narrow",void 0),t([le()],ln.prototype,"config",void 0),t([le()],ln.prototype,"item",void 0),t([le()],ln.prototype,"areas",void 0),t([le()],ln.prototype,"alarmoConfig",void 0),t([le()],ln.prototype,"viewMode",void 0),t([le()],ln.prototype,"errors",void 0),ln=t([re("automation-editor-card")],ln);let dn=class extends(et(ne)){constructor(){super(...arguments),this.areas={},this.getAreaForAutomation=e=>{if(!this.config)return;const t=Fs(this.areas,this.config);let a=e.triggers[0].area;return Ci(a)&&t.includes(a)?a:void 0}}hassSubscribe(){return this._fetchData(),[this.hass.connection.subscribeMessage(()=>this._fetchData(),{type:"alarmo_config_updated"})]}async _fetchData(){if(!this.hass)return;const e=await He(this.hass);this.automations=Object.values(e),this.areas=await Ze(this.hass),this.config=await Ge(this.hass)}firstUpdated(){var e;this.path.filter&&(this.selectedArea=null===(e=this.path.filter)||void 0===e?void 0:e.area),(async()=>{await Ie()})()}render(){if(!this.hass||!this.automations||!this.config)return q``;if("new_notification"==this.path.subpage)return q`
`;if(this.path.params.edit_notification){const e=this.automations.find(e=>e.automation_id==this.path.params.edit_notification&&e.type==wi.Notification);return q`
`}if("new_action"==this.path.subpage)return q`
`;if(this.path.params.edit_action){const e=this.automations.find(e=>e.automation_id==this.path.params.edit_action&&e.type==wi.Action);return q`
`}{const e=()=>q`
${gi("panels.actions.cards.notifications.table.no_area_warning",this.hass.language)}
`,t={type:{width:"40px",renderer:t=>"no_area"!=t.area||this.config.master.enabled?t.type==wi.Notification?q`
`:q`
`:q`
${e()}
`},name:{title:this.hass.localize("ui.components.area-picker.add_dialog.name"),renderer:t=>q`
${"no_area"!=t.area||this.config.master.enabled?"":e()}
${t.name}
`,width:"40%",grow:!0,text:!0},enabled:{title:gi("common.enabled",this.hass.language),width:"68px",align:"center",renderer:e=>q`
{t.stopPropagation(),this.toggleEnable(t,e.automation_id)}}
>
`}},a=this.automations.filter(e=>e.type==wi.Notification).map(e=>Object(Object.assign(Object.assign({},e),{id:e.automation_id,warning:!this.config.master.enabled&&!this.getAreaForAutomation(e),area:this.getAreaForAutomation(e)||"no_area"}))),i=this.automations.filter(e=>e.type==wi.Action).map(e=>Object(Object.assign(Object.assign({},e),{id:e.automation_id,warning:!this.config.master.enabled&&!this.getAreaForAutomation(e),area:this.getAreaForAutomation(e)||"no_area"})));return q`
${gi("panels.actions.cards.notifications.description",this.hass.language)}
Pe(0,Ui("actions",{params:{edit_notification:e.detail.id}}),!0)}
>
${gi("panels.actions.cards.notifications.table.no_items",this.hass.language)}
${gi("panels.actions.cards.notifications.actions.new_notification",this.hass.language)}
${gi("panels.actions.cards.actions.description",this.hass.language)}
Pe(0,Ui("actions",{params:{edit_action:e.detail.id}}),!0)}
>
${gi("panels.actions.cards.actions.table.no_items",this.hass.language)}
${gi("panels.actions.cards.actions.actions.new_action",this.hass.language)}
`}}toggleEnable(e,t){We(this.hass,{automation_id:t,enabled:!e.target.checked}).catch(t=>Di(t,e)).then()}getTableFilterOptions(){if(!this.hass)return;let e=Object.values(this.areas).map(e=>Object({value:e.area_id,name:e.name,badge:t=>t.filter(t=>t.area==e.area_id).length})).sort(Pi);Object.values(this.automations||[]).filter(e=>!this.getAreaForAutomation(e)).length&&(e=[{value:"no_area",name:this.config.master.enabled?this.config.master.name:this.hass.localize("state_attributes.climate.preset_mode.none"),badge:e=>e.filter(e=>"no_area"==e.area).length},...e]);return{area:{name:gi("components.table.filter.item",this.hass.language,"name",gi("panels.actions.cards.new_action.fields.area.heading",this.hass.language)),items:e,value:this.selectedArea?[this.selectedArea]:[]}}}addNotificationClick(){Pe(0,Ui("actions","new_notification"),!0)}addActionClick(){Pe(0,Ui("actions","new_action"),!0)}};dn.styles=qi,t([le()],dn.prototype,"hass",void 0),t([le()],dn.prototype,"narrow",void 0),t([le()],dn.prototype,"path",void 0),t([le()],dn.prototype,"alarmEntity",void 0),t([le()],dn.prototype,"automations",void 0),t([le()],dn.prototype,"areas",void 0),t([le()],dn.prototype,"config",void 0),t([le()],dn.prototype,"selectedArea",void 0),dn=t([re("alarm-view-actions")],dn),e.MyAlarmPanel=class extends ne{async firstUpdated(){window.addEventListener("location-changed",()=>{this.requestUpdate()}),await Ie(),this.userConfig=await Ve(this.hass),this.requestUpdate()}render(){if(!customElements.get("ha-app-layout")||!this.userConfig)return q`
loading...
`;const e=Ii();return q`
${gi("title",this.hass.language)}
v${"1.9.5"}
${gi("panels.general.title",this.hass.language)}
${gi("panels.sensors.title",this.hass.language)}
${gi("panels.codes.title",this.hass.language)}
${gi("panels.actions.title",this.hass.language)}
${this.getView(e)}
`}getView(e){switch(e.page){case"general":return q`
`;case"sensors":return q`
`;case"codes":return q`
`;case"actions":return q`
`;default:return q`
The page you are trying to reach cannot be found. Please select a page from the menu above to continue.
`}}handlePageSelected(e){const t=e.detail.item.getAttribute("page-name");t!==Ii()?(Pe(0,Ui(t)),this.requestUpdate()):scrollTo(0,0)}static get styles(){return o`
${qi} :host {
color: var(--primary-text-color);
--paper-card-header-color: var(--primary-text-color);
}
app-header,
app-toolbar {
background-color: var(--app-header-background-color);
font-weight: 400;
color: var(--app-header-text-color, white);
}
app-toolbar {
height: var(--header-height);
}
ha-app-layout {
display: block;
z-index: 2;
}
app-toolbar [main-title] {
margin-left: 20px;
}
ha-tabs {
margin-left: max(env(safe-area-inset-left), 24px);
margin-right: max(env(safe-area-inset-right), 24px);
--paper-tabs-selection-bar-color: var(--app-header-selection-bar-color, var(--app-header-text-color, #fff));
text-transform: uppercase;
}
.view {
height: calc(100vh - 112px);
display: flex;
justify-content: center;
}
.view > * {
width: 600px;
max-width: 600px;
}
.view > *:last-child {
margin-bottom: 20px;
}
.version {
font-size: 14px;
font-weight: 500;
color: rgba(var(--rgb-text-primary-color), 0.9);
}
`}},t([le()],e.MyAlarmPanel.prototype,"hass",void 0),t([le({type:Boolean,reflect:!0})],e.MyAlarmPanel.prototype,"narrow",void 0),t([le()],e.MyAlarmPanel.prototype,"userConfig",void 0),e.MyAlarmPanel=t([re("alarm-panel")],e.MyAlarmPanel)}({});
================================================
FILE: custom_components/alarmo/helpers.py
================================================
import logging
from homeassistant.core import (
HomeAssistant,
)
from . import const
_LOGGER = logging.getLogger(__name__)
def friendly_name_for_entity_id(entity_id: str, hass: HomeAssistant):
"""helper to get friendly name for entity"""
state = hass.states.get(entity_id)
if state and state.attributes["friendly_name"]:
return state.attributes["friendly_name"]
return entity_id
def omit(obj: dict, blacklisted_keys: list):
return {
key: val
for key, val in obj.items()
if key not in blacklisted_keys
}
================================================
FILE: custom_components/alarmo/manifest.json
================================================
{
"domain": "alarmo",
"name": "Alarmo",
"documentation": "https://github.com/nielsfaber/alarmo",
"issue_tracker": "https://github.com/nielsfaber/alarmo/issues",
"version": "v1.9.5",
"dependencies": [
"http",
"panel_custom"
],
"after_dependencies": [
"mqtt",
"notify"
],
"codeowners": [
"@nielsfaber"
],
"requirements": [],
"config_flow": true,
"iot_class": "local_push"
}
================================================
FILE: custom_components/alarmo/mqtt.py
================================================
import json
import logging
from homeassistant.core import (
HomeAssistant,
callback,
)
from homeassistant.components.mqtt import (
DOMAIN as ATTR_MQTT,
CONF_STATE_TOPIC,
CONF_COMMAND_TOPIC,
)
import homeassistant.components.mqtt as mqtt
from homeassistant.helpers.json import JSONEncoder
from homeassistant.util import slugify
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import const
from .helpers import (
friendly_name_for_entity_id,
)
_LOGGER = logging.getLogger(__name__)
CONF_EVENT_TOPIC = "event_topic"
class MqttHandler:
def __init__(self, hass: HomeAssistant):
self.hass = hass
self._config = None
self._subscribed_topics = []
self._subscriptions = []
async def async_update_config(_args=None):
"""mqtt config updated, reload the configuration."""
old_config = self._config
new_config = self.hass.data[const.DOMAIN]["coordinator"].store.async_get_config()
if old_config and old_config[ATTR_MQTT] == new_config[ATTR_MQTT]:
# only update MQTT config if some parameters are changed
return
self._config = new_config
if not old_config or old_config[ATTR_MQTT][CONF_COMMAND_TOPIC] != new_config[ATTR_MQTT][CONF_COMMAND_TOPIC]:
# re-subscribing is only needed if the command topic has changed
await self._async_subscribe_topics()
_LOGGER.debug("MQTT config was (re)loaded")
self._subscriptions.append(
async_dispatcher_connect(hass, "alarmo_config_updated", async_update_config)
)
self.hass.async_add_job(async_update_config)
@callback
def async_alarm_state_changed(area_id: str, old_state: str, new_state: str):
if not self._config[ATTR_MQTT][const.ATTR_ENABLED]:
return
topic = self._config[ATTR_MQTT][CONF_STATE_TOPIC]
if not topic: # do not publish if no topic is provided
return
if area_id and len(self.hass.data[const.DOMAIN]["areas"]) > 1:
# handle the sending of a state update for a specific area
area = self.hass.data[const.DOMAIN]["areas"][area_id]
topic = topic.rsplit('/', 1)
topic.insert(1, slugify(area.name))
topic = "/".join(topic)
payload_config = self._config[ATTR_MQTT][const.ATTR_STATE_PAYLOAD]
if new_state in payload_config and payload_config[new_state]:
message = payload_config[new_state]
else:
message = new_state
hass.async_create_task(mqtt.async_publish(self.hass, topic, message, retain=True))
_LOGGER.debug("Published state '{}' on topic '{}'".format(message, topic))
self._subscriptions.append(
async_dispatcher_connect(self.hass, "alarmo_state_updated", async_alarm_state_changed)
)
@callback
def async_handle_event(event: str, area_id: str, args: dict = {}):
if not self._config[ATTR_MQTT][const.ATTR_ENABLED]:
return
topic = self._config[ATTR_MQTT][CONF_EVENT_TOPIC]
if not topic: # do not publish if no topic is provided
return
if area_id and len(self.hass.data[const.DOMAIN]["areas"]) > 1:
# handle the sending of a state update for a specific area
area = self.hass.data[const.DOMAIN]["areas"][area_id]
topic = topic.rsplit('/', 1)
topic.insert(1, slugify(area.name))
topic = "/".join(topic)
if event == const.EVENT_ARM:
payload = {
"event": "{}_{}".format(
event.upper(),
args["arm_mode"].split("_", 1).pop(1).upper()
),
"delay": args["delay"],
}
elif event == const.EVENT_TRIGGER:
payload = {
"event": event.upper(),
"delay": args["delay"],
"sensors": [
{
"entity_id": entity,
"name": friendly_name_for_entity_id(entity, self.hass),
}
for (entity, state) in args["open_sensors"].items()
]
}
elif event == const.EVENT_FAILED_TO_ARM:
payload = {
"event": event.upper(),
"sensors": [
{
"entity_id": entity,
"name": friendly_name_for_entity_id(entity, self.hass),
}
for (entity, state) in args["open_sensors"].items()
]
}
elif event == const.EVENT_COMMAND_NOT_ALLOWED:
payload = {
"event": event.upper(),
"state": args["state"],
"command": args["command"].upper()
}
elif event in [const.EVENT_INVALID_CODE_PROVIDED, const.EVENT_NO_CODE_PROVIDED]:
payload = {
"event": event.upper()
}
else:
return
payload = json.dumps(payload, cls=JSONEncoder)
hass.async_create_task(mqtt.async_publish(self.hass, topic, payload))
self._subscriptions.append(
async_dispatcher_connect(self.hass, "alarmo_event", async_handle_event)
)
def __del__(self):
"""prepare for removal"""
while len(self._subscribed_topics):
self._subscribed_topics.pop()()
while len(self._subscriptions):
self._subscriptions.pop()()
async def _async_subscribe_topics(self):
"""install a listener for the command topic."""
if len(self._subscribed_topics):
while len(self._subscribed_topics):
self._subscribed_topics.pop()()
_LOGGER.debug("Removed subscribed topics")
if not self._config[ATTR_MQTT][const.ATTR_ENABLED]:
return
self._subscribed_topics.append(
await mqtt.async_subscribe(
self.hass,
self._config[ATTR_MQTT][CONF_COMMAND_TOPIC],
self.async_message_received,
)
)
_LOGGER.debug("Subscribed to topic {}".format(self._config[ATTR_MQTT][CONF_COMMAND_TOPIC]))
@callback
async def async_message_received(self, msg):
command = None
code = None
area = None
try:
payload = json.loads(msg.payload)
payload = {k.lower(): v for k, v in payload.items()}
if "command" in payload:
command = payload["command"]
elif "cmd" in payload:
command = payload["cmd"]
elif "action" in payload:
command = payload["action"]
elif "state" in payload:
command = payload["state"]
if "code" in payload:
code = payload["code"]
elif "pin" in payload:
code = payload["pin"]
elif "password" in payload:
code = payload["password"]
elif "pincode" in payload:
code = payload["pincode"]
if "area" in payload and payload["area"]:
area = payload["area"]
except ValueError:
# no JSON structure found
command = msg.payload
code = None
if type(command) is str:
command = command.lower()
else:
_LOGGER.warning("Received unexpected command")
return
payload_config = self._config[ATTR_MQTT][const.ATTR_COMMAND_PAYLOAD]
skip_code = not self._config[ATTR_MQTT][const.ATTR_REQUIRE_CODE]
command_payloads = {}
for item in const.COMMANDS:
if item in payload_config and payload_config[item]:
command_payloads[item] = payload_config[item].lower()
elif item not in payload_config:
command_payloads[item] = item.lower()
if command not in list(command_payloads.values()):
_LOGGER.warning("Received unexpected command: %s", command)
return
if area:
res = list(filter(lambda el: slugify(el.name) == area, self.hass.data[const.DOMAIN]["areas"].values()))
if not res:
_LOGGER.warning("Area {} does not exist".format(area))
return
entity = res[0]
else:
if self._config[const.ATTR_MASTER][const.ATTR_ENABLED] and len(self.hass.data[const.DOMAIN]["areas"]) > 1:
entity = self.hass.data[const.DOMAIN]["master"]
elif len(self.hass.data[const.DOMAIN]["areas"]) == 1:
entity = list(self.hass.data[const.DOMAIN]["areas"].values())[0]
else:
_LOGGER.warning("No area specified")
return
_LOGGER.debug("Received command {}".format(command))
if command == command_payloads[const.COMMAND_DISARM]:
await entity.async_alarm_disarm(code=code, skip_code=skip_code)
elif command == command_payloads[const.COMMAND_ARM_AWAY]:
await entity.async_alarm_arm_away(code, skip_code)
elif command == command_payloads[const.COMMAND_ARM_NIGHT]:
await entity.async_alarm_arm_night(code, skip_code)
elif command == command_payloads[const.COMMAND_ARM_HOME]:
await entity.async_alarm_arm_home(code, skip_code)
elif command == command_payloads[const.COMMAND_ARM_CUSTOM_BYPASS]:
await entity.async_alarm_arm_custom_bypass(code, skip_code)
elif command == command_payloads[const.COMMAND_ARM_VACATION]:
await entity.async_alarm_arm_vacation(code, skip_code)
================================================
FILE: custom_components/alarmo/panel.py
================================================
import os
import logging
from homeassistant.components import frontend
from homeassistant.components import panel_custom
from .const import (
CUSTOM_COMPONENTS,
INTEGRATION_FOLDER,
PANEL_FOLDER,
PANEL_URL,
PANEL_TITLE,
PANEL_ICON,
PANEL_NAME,
PANEL_FILENAME,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
async def async_register_panel(hass):
root_dir = os.path.join(hass.config.path(CUSTOM_COMPONENTS), INTEGRATION_FOLDER)
panel_dir = os.path.join(root_dir, PANEL_FOLDER)
view_url = os.path.join(panel_dir, PANEL_FILENAME)
hass.http.register_static_path(
PANEL_URL,
view_url,
cache_headers=False
)
await panel_custom.async_register_panel(
hass,
webcomponent_name=PANEL_NAME,
frontend_url_path=DOMAIN,
module_url=PANEL_URL,
sidebar_title=PANEL_TITLE,
sidebar_icon=PANEL_ICON,
require_admin=True,
config={},
)
def async_unregister_panel(hass):
frontend.async_remove_panel(hass, DOMAIN)
_LOGGER.debug("Removing panel")
================================================
FILE: custom_components/alarmo/sensors.py
================================================
import logging
import homeassistant.util.dt as dt_util
from homeassistant.core import (
HomeAssistant,
callback,
CoreState,
)
from homeassistant.helpers.event import (
async_track_state_change,
async_track_point_in_time,
)
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
)
from homeassistant.const import (
EVENT_HOMEASSISTANT_STARTED,
STATE_UNKNOWN,
STATE_UNAVAILABLE,
STATE_OPEN,
STATE_CLOSED,
STATE_ON,
STATE_OFF,
STATE_LOCKED,
STATE_UNLOCKED,
STATE_ALARM_PENDING,
STATE_ALARM_ARMING,
STATE_ALARM_TRIGGERED,
ATTR_STATE,
ATTR_LAST_TRIP_TIME,
ATTR_NAME,
)
from . import const
ATTR_USE_EXIT_DELAY = "use_exit_delay"
ATTR_USE_ENTRY_DELAY = "use_entry_delay"
ATTR_ALWAYS_ON = "always_on"
ATTR_ARM_ON_CLOSE = "arm_on_close"
ATTR_ALLOW_OPEN = "allow_open"
ATTR_TRIGGER_UNAVAILABLE = "trigger_unavailable"
ATTR_AUTO_BYPASS = "auto_bypass"
ATTR_AUTO_BYPASS_MODES = "auto_bypass_modes"
ATTR_GROUP = "group"
ATTR_GROUP_ID = "group_id"
ATTR_TIMEOUT = "timeout"
ATTR_EVENT_COUNT = "event_count"
ATTR_ENTITIES = "entities"
SENSOR_STATES_OPEN = [STATE_ON, STATE_OPEN, STATE_UNLOCKED]
SENSOR_STATES_CLOSED = [STATE_OFF, STATE_CLOSED, STATE_LOCKED]
SENSOR_TYPE_DOOR = "door"
SENSOR_TYPE_WINDOW = "window"
SENSOR_TYPE_MOTION = "motion"
SENSOR_TYPE_TAMPER = "tamper"
SENSOR_TYPE_ENVIRONMENTAL = "environmental"
SENSOR_TYPE_OTHER = "other"
SENSOR_TYPES = [
SENSOR_TYPE_DOOR,
SENSOR_TYPE_WINDOW,
SENSOR_TYPE_MOTION,
SENSOR_TYPE_TAMPER,
SENSOR_TYPE_ENVIRONMENTAL,
SENSOR_TYPE_OTHER,
]
_LOGGER = logging.getLogger(__name__)
def parse_sensor_state(state):
if not state or not state.state:
return STATE_UNKNOWN
elif state.state == STATE_UNAVAILABLE:
return STATE_UNAVAILABLE
elif state.state in SENSOR_STATES_OPEN:
return STATE_OPEN
elif state.state in SENSOR_STATES_CLOSED:
return STATE_CLOSED
else:
return STATE_UNKNOWN
def sensor_state_allowed(state, sensor_config, alarm_state):
"""return whether the sensor state is permitted or a state change should occur"""
if state != STATE_OPEN and (state != STATE_UNAVAILABLE or not sensor_config[ATTR_TRIGGER_UNAVAILABLE]):
# sensor has the safe state
return True
elif alarm_state == STATE_ALARM_TRIGGERED:
# alarm is already triggered
return True
elif sensor_config[ATTR_ALWAYS_ON]:
# alarm should always be triggered by always-on sensor
return False
elif alarm_state == STATE_ALARM_ARMING and not sensor_config[ATTR_USE_EXIT_DELAY]:
# arming should be aborted if sensor without exit delay is active
return False
elif alarm_state in const.ARM_MODES:
# normal triggering case
return False
elif alarm_state == STATE_ALARM_PENDING and not sensor_config[ATTR_USE_ENTRY_DELAY]:
# triggering of immediate sensor while alarm is pending
return False
else:
return True
class SensorHandler:
def __init__(self, hass: HomeAssistant):
self._config = None
self.hass = hass
self._state_listener = None
self._subscriptions = []
self._arm_timers = {}
self._groups = {}
self._group_events = {}
self._startup_complete = False
def async_update_sensor_config():
"""sensor config updated, reload the configuration."""
self._config = self.hass.data[const.DOMAIN]["coordinator"].store.async_get_sensors()
self._groups = self.hass.data[const.DOMAIN]["coordinator"].store.async_get_sensor_groups()
self._group_events = {}
self.async_watch_sensor_states()
self._subscriptions.append(
async_dispatcher_connect(hass, "alarmo_state_updated", self.async_watch_sensor_states)
)
self._subscriptions.append(
async_dispatcher_connect(hass, "alarmo_sensors_updated", async_update_sensor_config)
)
async_update_sensor_config()
def handle_startup(_event):
self._startup_complete = True
if hass.state == CoreState.running:
self._startup_complete = True
else:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, handle_startup)
def __del__(self):
"""prepare for removal"""
if self._state_listener:
self._state_listener()
self._state_listener = None
while len(self._subscriptions):
self._subscriptions.pop()()
def async_watch_sensor_states(self, area_id: str = None, old_state: str = None, state: str = None):
"""watch sensors based on the state of the alarm entities."""
sensors_list = []
for area in self.hass.data[const.DOMAIN]["areas"].keys():
sensors_list.extend(self.active_sensors_for_alarm_state(area))
if self._state_listener:
self._state_listener()
if len(sensors_list):
self._state_listener = async_track_state_change(
self.hass, sensors_list, self.async_sensor_state_changed
)
else:
self._state_listener = None
# clear previous sensor group events which are not active for current alarm state
for group_id in self._group_events.keys():
self._group_events[group_id] = dict(filter(
lambda el: el[0] in sensors_list,
self._group_events[group_id].items()
))
# handle initial sensor states
if area_id and old_state is None:
sensors_list = self.active_sensors_for_alarm_state(area_id)
for entity in sensors_list:
sensor_state = parse_sensor_state(self.hass.states.get(entity))
if sensor_state != STATE_UNKNOWN:
_LOGGER.debug("Initial state for {} is {}".format(entity, state))
def active_sensors_for_alarm_state(self, area_id: str, state: str = None):
"""Compose a list of sensors that are active for the state"""
alarm_entity = self.hass.data[const.DOMAIN]["areas"][area_id]
if not state:
state = alarm_entity.arm_mode if alarm_entity.arm_mode else alarm_entity.state
entities = []
for entity, config in self._config.items():
if config["area"] != area_id or not config["enabled"]:
continue
elif alarm_entity.bypassed_sensors and entity in alarm_entity.bypassed_sensors:
continue
elif (
state in config[const.ATTR_MODES]
or config[ATTR_ALWAYS_ON]
):
entities.append(entity)
return entities
def validate_arming_event(self, area_id: str, target_state: str = None, **kwargs):
"""check whether all sensors have the correct state prior to arming."""
use_delay = kwargs.get("use_delay", False)
bypass_open_sensors = kwargs.get("bypass_open_sensors", False)
sensors_list = self.active_sensors_for_alarm_state(area_id, target_state)
open_sensors = {}
bypassed_sensors = []
alarm_state = target_state
if use_delay and alarm_state in const.ARM_MODES:
alarm_state = STATE_ALARM_ARMING
elif use_delay and alarm_state == STATE_ALARM_TRIGGERED:
alarm_state = STATE_ALARM_PENDING
for entity in sensors_list:
sensor_config = self._config[entity]
sensor_state = parse_sensor_state(self.hass.states.get(entity))
res = sensor_state_allowed(sensor_state, sensor_config, alarm_state)
if not res and target_state in const.ARM_MODES:
# sensor is active while arming
if sensor_config[ATTR_ALLOW_OPEN]:
# sensor is permitted to be open during/after arming
continue
elif bypass_open_sensors or (
sensor_config[ATTR_AUTO_BYPASS] and
target_state in sensor_config[ATTR_AUTO_BYPASS_MODES]
):
# sensor may be bypassed
bypassed_sensors.append(entity)
else:
open_sensors[entity] = sensor_state
return (open_sensors, bypassed_sensors)
@callback
async def async_sensor_state_changed(self, entity, old_state, new_state):
"""Callback fired when a sensor state has changed."""
old_state = parse_sensor_state(old_state)
new_state = parse_sensor_state(new_state)
if old_state == STATE_UNKNOWN:
# sensor is unknown at startup, state which comes after is considered as initial state
_LOGGER.debug("Initial state for {} is {}".format(entity, new_state))
return
if old_state == new_state:
# not a state change - ignore
return
_LOGGER.debug("entity {} changed: old_state={}, new_state={}".format(entity, old_state, new_state))
sensor_config = self._config[entity]
alarm_entity = self.hass.data[const.DOMAIN]["areas"][sensor_config["area"]]
alarm_state = alarm_entity.state
res = sensor_state_allowed(new_state, sensor_config, alarm_state)
if sensor_config[ATTR_ARM_ON_CLOSE] and alarm_state == STATE_ALARM_ARMING:
# we are arming and sensor is configured to arm on closing
if new_state == STATE_CLOSED:
self.start_arm_timer(entity)
else:
self.stop_arm_timer(entity)
if res:
# nothing to do here, sensor state is OK
return
open_sensors = self.process_group_event(entity, new_state)
if not open_sensors:
# triggered sensor is part of a group and should be ignored
return
if sensor_config[ATTR_ALWAYS_ON]:
# immediate trigger due to always on sensor
_LOGGER.info("Alarm is triggered due to an always-on sensor: {}".format(entity))
await alarm_entity.async_trigger(
skip_delay=True,
open_sensors=open_sensors
)
elif alarm_state == STATE_ALARM_ARMING:
# sensor triggered while arming, abort arming
_LOGGER.debug("Arming was aborted due to a sensor being active: {}".format(entity))
await alarm_entity.async_arm_failure(open_sensors)
elif alarm_state in const.ARM_MODES:
# standard alarm trigger
_LOGGER.info("Alarm is triggered due to sensor: {}".format(entity))
await alarm_entity.async_trigger(
skip_delay=(not sensor_config[ATTR_USE_ENTRY_DELAY]),
open_sensors=open_sensors
)
elif alarm_state == STATE_ALARM_PENDING:
# immediate trigger while in pending state
_LOGGER.info("Alarm is triggered due to sensor: {}".format(entity))
await alarm_entity.async_trigger(
skip_delay=True,
open_sensors=open_sensors
)
def start_arm_timer(self, entity):
"""start timer for automatical arming"""
@callback
async def timer_finished(now):
_LOGGER.debug("timer finished")
sensor_config = self._config[entity]
alarm_entity = self.hass.data[const.DOMAIN]["areas"][sensor_config["area"]]
if alarm_entity.state == STATE_ALARM_ARMING:
await alarm_entity.async_arm(alarm_entity.arm_mode, skip_delay=True)
now = dt_util.utcnow()
if entity in self._arm_timers:
self.stop_arm_timer(entity)
self._arm_timers[entity] = async_track_point_in_time(
self.hass, timer_finished, now + const.SENSOR_ARM_TIME
)
def stop_arm_timer(self, entity=None):
"""cancel timer(s) for automatical arming"""
if entity and entity in self._arm_timers:
self._arm_timers[entity]()
elif not entity:
for entity in self._arm_timers.keys():
self._arm_timers[entity]()
def process_group_event(self, entity: str, state: str) -> dict:
"""check if sensor entity is member of a group and compare with previous events to evaluate trigger"""
group_id = None
for group in self._groups.values():
if entity in group[ATTR_ENTITIES]:
group_id = group[ATTR_GROUP_ID]
break
open_sensors = {
entity: state
}
if group_id is None:
return open_sensors
group = self._groups[group_id]
group_events = self._group_events[group_id] if group_id in self._group_events.keys() else {}
now = dt_util.now()
group_events[entity] = {
ATTR_STATE: state,
ATTR_LAST_TRIP_TIME: now
}
self._group_events[group_id] = group_events
recent_events = {
entity: (now - event[ATTR_LAST_TRIP_TIME]).total_seconds()
for (entity, event) in group_events.items()
}
recent_events = dict(filter(lambda el: el[1] <= group[ATTR_TIMEOUT], recent_events.items()))
if len(recent_events.keys()) < group[ATTR_EVENT_COUNT]:
_LOGGER.debug("tripped sensor {} was ignored since it belongs to group {}".format(entity, group[ATTR_NAME]))
return {}
else:
for entity in recent_events.keys():
open_sensors[entity] = group_events[entity][ATTR_STATE]
_LOGGER.debug("tripped sensor {} caused the triggering of group {}".format(entity, group[ATTR_NAME]))
return open_sensors
================================================
FILE: custom_components/alarmo/services.yaml
================================================
arm:
name: Arm
description: "Arm an Alarmo entity with custom settings."
fields:
entity_id:
name: Entity ID
description: Name of entity that should be armed.
example: "alarm_control_panel.alarm"
required: true
selector:
entity:
integration: alarmo
domain: alarm_control_panel
code:
name: Code
description: Code to arm the alarm with.
example: "1234"
required: false
selector:
text:
mode:
name: Mode
description: "Mode to arm the alarm in."
example: "away"
required: false
default: away
selector:
select:
options:
- away
- night
- home
- vacation
- custom
skip_delay:
name: Skip Delay
description: "Skip the exit delay."
example: true
required: false
default: false
selector:
boolean:
force:
name: Force
description: "Automatically bypass all sensors that prevent the arming operation."
example: true
required: false
default: false
selector:
boolean:
disarm:
name: Disarm
description: "Disarm an Alarmo entity."
fields:
entity_id:
name: Entity ID
description: Name of entity that should be disarmed.
example: "alarm_control_panel.alarm"
required: true
selector:
entity:
integration: alarmo
domain: alarm_control_panel
code:
name: Code
description: Code to disarm the alarm with.
example: "1234"
required: false
selector:
text:
enable_user:
name: Enable User
description: "Allow a user to arm/disarm alarmo."
fields:
name:
name: Name
description: Name of the user to enable.
example: "Frank"
required: true
selector:
text:
disable_user:
name: Disable User
description: "Block a user from arming/disarming alarmo."
fields:
name:
name: Name
description: Name of the user to disable.
example: "Frank"
required: true
selector:
text:
================================================
FILE: custom_components/alarmo/store.py
================================================
import logging
import time
import attr
from collections import OrderedDict
from typing import MutableMapping, cast
from homeassistant.loader import bind_hass
from homeassistant.core import (callback, HomeAssistant)
from homeassistant.helpers.storage import Store
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_VACATION
)
from homeassistant.components.alarm_control_panel import (
FORMAT_NUMBER as CODE_FORMAT_NUMBER,
)
from .const import DOMAIN
from .sensors import (
SENSOR_TYPE_OTHER,
)
from .helpers import omit
_LOGGER = logging.getLogger(__name__)
DATA_REGISTRY = f"{DOMAIN}_storage"
STORAGE_KEY = f"{DOMAIN}.storage"
STORAGE_VERSION = 6
SAVE_DELAY = 10
@attr.s(slots=True, frozen=True)
class ModeEntry:
"""Mode storage Entry."""
enabled = attr.ib(type=bool, default=False)
exit_time = attr.ib(type=int, default=0)
entry_time = attr.ib(type=int, default=0)
trigger_time = attr.ib(type=int, default=0)
@attr.s(slots=True, frozen=True)
class MqttConfig:
"""MQTT storage Entry."""
enabled = attr.ib(type=bool, default=False)
state_topic = attr.ib(type=str, default="alarmo/state")
state_payload = attr.ib(type=dict, default={})
command_topic = attr.ib(type=str, default="alarmo/command")
command_payload = attr.ib(type=dict, default={})
require_code = attr.ib(type=bool, default=True)
event_topic = attr.ib(type=str, default="alarmo/event")
@attr.s(slots=True, frozen=True)
class MasterConfig:
"""Master storage Entry."""
enabled = attr.ib(type=bool, default=True)
name = attr.ib(type=str, default="master")
@attr.s(slots=True, frozen=True)
class AreaEntry:
"""Area storage Entry."""
area_id = attr.ib(type=str, default=None)
name = attr.ib(type=str, default=None)
modes = attr.ib(type=[str, ModeEntry], default={
STATE_ALARM_ARMED_AWAY: ModeEntry(),
STATE_ALARM_ARMED_HOME: ModeEntry(),
STATE_ALARM_ARMED_NIGHT: ModeEntry(),
STATE_ALARM_ARMED_CUSTOM_BYPASS: ModeEntry(),
STATE_ALARM_ARMED_VACATION: ModeEntry()
})
@attr.s(slots=True, frozen=True)
class Config:
"""(General) Config storage Entry."""
code_arm_required = attr.ib(type=bool, default=False)
code_disarm_required = attr.ib(type=bool, default=False)
code_format = attr.ib(type=str, default=CODE_FORMAT_NUMBER)
disarm_after_trigger = attr.ib(type=bool, default=False)
master = attr.ib(type=MasterConfig, default=MasterConfig())
mqtt = attr.ib(type=MqttConfig, default=MqttConfig())
@attr.s(slots=True, frozen=True)
class SensorEntry:
"""Sensor storage Entry."""
entity_id = attr.ib(type=str, default=None)
type = attr.ib(type=str, default=SENSOR_TYPE_OTHER)
modes = attr.ib(type=list, default=[])
use_exit_delay = attr.ib(type=bool, default=True)
use_entry_delay = attr.ib(type=bool, default=True)
always_on = attr.ib(type=bool, default=False)
arm_on_close = attr.ib(type=bool, default=False)
allow_open = attr.ib(type=bool, default=False)
trigger_unavailable = attr.ib(type=bool, default=False)
auto_bypass = attr.ib(type=bool, default=False)
auto_bypass_modes = attr.ib(type=list, default=[])
area = attr.ib(type=str, default=None)
enabled = attr.ib(type=bool, default=True)
@attr.s(slots=True, frozen=True)
class UserEntry:
"""User storage Entry."""
user_id = attr.ib(type=str, default=None)
name = attr.ib(type=str, default="")
enabled = attr.ib(type=bool, default=True)
code = attr.ib(type=str, default="")
can_arm = attr.ib(type=bool, default=False)
can_disarm = attr.ib(type=bool, default=False)
is_override_code = attr.ib(type=bool, default=False)
code_format = attr.ib(type=str, default="")
code_length = attr.ib(type=int, default=0)
area_limit = attr.ib(type=list, default=[])
@attr.s(slots=True, frozen=True)
class AlarmoTriggerEntry:
"""Trigger storage Entry."""
event = attr.ib(type=str, default="")
area = attr.ib(type=str, default=None)
modes = attr.ib(type=list, default=[])
@attr.s(slots=True, frozen=True)
class EntityTriggerEntry:
"""Trigger storage Entry."""
entity_id = attr.ib(type=str, default=None)
state = attr.ib(type=str, default=None)
@attr.s(slots=True, frozen=True)
class ActionEntry:
"""Action storage Entry."""
service = attr.ib(type=str, default="")
entity_id = attr.ib(type=str, default=None)
data = attr.ib(type=dict, default={})
@attr.s(slots=True, frozen=True)
class AutomationEntry:
"""Automation storage Entry."""
automation_id = attr.ib(type=str, default=None)
type = attr.ib(type=str, default=None)
name = attr.ib(type=str, default="")
triggers = attr.ib(type=[AlarmoTriggerEntry], default=[])
actions = attr.ib(type=[ActionEntry], default=[])
enabled = attr.ib(type=bool, default=True)
@attr.s(slots=True, frozen=True)
class SensorGroupEntry:
"""Sensor group storage Entry."""
group_id = attr.ib(type=str, default=None)
name = attr.ib(type=str, default="")
entities = attr.ib(type=list, default=[])
timeout = attr.ib(type=int, default=0)
event_count = attr.ib(type=int, default=2)
def parse_automation_entry(data: dict):
def create_trigger_entity(config: dict):
if "event" in config:
return AlarmoTriggerEntry(**config)
else:
return EntityTriggerEntry(**config)
output = {}
if "triggers" in data:
output["triggers"] = list(map(create_trigger_entity, data["triggers"]))
if "actions" in data:
output["actions"] = list(map(lambda el: ActionEntry(**el), data["actions"]))
if "automation_id" in data:
output["automation_id"] = data["automation_id"]
if "name" in data:
output["name"] = data["name"]
if "type" in data:
output["type"] = data["type"]
if "enabled" in data:
output["enabled"] = data["enabled"]
return output
class MigratableStore(Store):
async def _async_migrate_func(self, old_version, data: dict):
if old_version == 1:
area_id = str(int(time.time()))
data["areas"] = [
attr.asdict(AreaEntry(**{
"name": "Alarmo",
"modes": {
mode: attr.asdict(ModeEntry(
enabled=bool(config["enabled"]),
exit_time=int(config["leave_time"]),
entry_time=int(config["entry_time"]),
trigger_time=int(data["config"]["trigger_time"])
))
for (mode, config) in data["config"]["modes"].items()
}
}, area_id=area_id))
]
if "sensors" in data:
for sensor in data["sensors"]:
sensor["area"] = area_id
if old_version <= 2:
data["automations"] = [
attr.asdict(AutomationEntry(
**parse_automation_entry({
**automation,
**{
"triggers": list(map(
lambda el: attr.asdict(AlarmoTriggerEntry(
event=el["state"] if "state" in el else el["event"],
area=automation["area"] if "area" in el else None,
modes=automation["modes"]
)),
automation["triggers"]
)),
"type": "notification"
if "is_notification" in automation and automation["is_notification"]
else "action"
}
})
))
for automation in data["automations"]
]
if old_version <= 3:
data["sensors"] = [
attr.asdict(SensorEntry(
**{
**omit(sensor, ["immediate", "name"]),
"use_exit_delay": not sensor["immediate"] and not sensor["always_on"],
"use_entry_delay": not sensor["immediate"] and not sensor["always_on"],
"auto_bypass_modes": sensor["modes"]
if "auto_bypass" in sensor and sensor["auto_bypass"]
else [],
}
))
for sensor in data["sensors"]
]
if old_version <= 4:
data["sensors"] = [
attr.asdict(SensorEntry(
**omit(sensor, ["name"]),
))
for sensor in data["sensors"]
]
if old_version <= 5:
data["automations"] = [
attr.asdict(AutomationEntry(
**parse_automation_entry({
**automation,
**{
"actions": list(map(
lambda el: attr.asdict(ActionEntry(
service=el["service"],
entity_id=el["entity_id"],
data=el["service_data"]
)),
automation["actions"]
))
}
})
))
for automation in data["automations"]
]
return data
class AlarmoStorage:
"""Class to hold alarmo configuration data."""
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the storage."""
self.hass = hass
self.config: Config = Config()
self.areas: MutableMapping[str, AreaEntry] = {}
self.sensors: MutableMapping[str, SensorEntry] = {}
self.users: MutableMapping[str, UserEntry] = {}
self.automations: MutableMapping[str, AutomationEntry] = {}
self.sensor_groups: MutableMapping[str, SensorGroupEntry] = {}
self._store = MigratableStore(hass, STORAGE_VERSION, STORAGE_KEY)
async def async_load(self) -> None:
"""Load the registry of schedule entries."""
data = await self._store.async_load()
config: Config = Config()
areas: "OrderedDict[str, AreaEntry]" = OrderedDict()
sensors: "OrderedDict[str, SensorEntry]" = OrderedDict()
users: "OrderedDict[str, UserEntry]" = OrderedDict()
automations: "OrderedDict[str, AutomationEntry]" = OrderedDict()
sensor_groups: "OrderedDict[str, SensorGroupEntry]" = OrderedDict()
if data is not None:
config = Config(
code_arm_required=data["config"]["code_arm_required"],
code_disarm_required=data["config"]["code_disarm_required"],
code_format=data["config"]["code_format"],
disarm_after_trigger=data["config"]["disarm_after_trigger"]
)
if "mqtt" in data["config"]:
config = attr.evolve(config, **{
"mqtt": MqttConfig(**data["config"]["mqtt"]),
})
if "master" in data["config"]:
config = attr.evolve(config, **{
"master": MasterConfig(**data["config"]["master"]),
})
if "areas" in data:
for area in data["areas"]:
modes = {
mode: ModeEntry(
enabled=config["enabled"],
exit_time=config["exit_time"],
entry_time=config["entry_time"],
trigger_time=config["trigger_time"]
)
for (mode, config) in area["modes"].items()
}
areas[area["area_id"]] = AreaEntry(
area_id=area["area_id"],
name=area["name"],
modes=modes
)
if "sensors" in data:
for sensor in data["sensors"]:
sensors[sensor["entity_id"]] = SensorEntry(**sensor)
if "users" in data:
for user in data["users"]:
users[user["user_id"]] = UserEntry(**omit(user, ["is_admin"]))
if "automations" in data:
for automation in data["automations"]:
automations[automation["automation_id"]] = AutomationEntry(**parse_automation_entry(automation))
if "sensor_groups" in data:
for group in data["sensor_groups"]:
sensor_groups[group["group_id"]] = SensorGroupEntry(**group)
self.config = config
self.areas = areas
self.sensors = sensors
self.automations = automations
self.users = users
self.sensor_groups = sensor_groups
if not areas:
await self.async_factory_default()
async def async_factory_default(self):
self.async_create_area({
"name": "Alarmo",
"modes": {
STATE_ALARM_ARMED_AWAY: attr.asdict(
ModeEntry(
enabled=True,
exit_time=60,
entry_time=60,
trigger_time=1800
)
),
STATE_ALARM_ARMED_HOME: attr.asdict(
ModeEntry(
enabled=True,
trigger_time=1800
)
)
}
})
@callback
def async_schedule_save(self) -> None:
"""Schedule saving the registry of alarmo."""
self._store.async_delay_save(self._data_to_save, SAVE_DELAY)
async def async_save(self) -> None:
"""Save the registry of alarmo."""
await self._store.async_save(self._data_to_save())
@callback
def _data_to_save(self) -> dict:
"""Return data for the registry for alarmo to store in a file."""
store_data = {
"config": attr.asdict(self.config),
}
store_data["areas"] = [
attr.asdict(entry) for entry in self.areas.values()
]
store_data["sensors"] = [
attr.asdict(entry) for entry in self.sensors.values()
]
store_data["users"] = [
attr.asdict(entry) for entry in self.users.values()
]
store_data["automations"] = [
attr.asdict(entry) for entry in self.automations.values()
]
store_data["sensor_groups"] = [
attr.asdict(entry) for entry in self.sensor_groups.values()
]
return store_data
async def async_delete(self):
"""Delete config."""
_LOGGER.warning("Removing alarmo configuration data!")
await self._store.async_remove()
self.config = Config()
self.areas = {}
self.sensors = {}
self.users = {}
self.automations = {}
self.sensor_groups = {}
await self.async_factory_default()
@callback
def async_get_config(self):
return attr.asdict(self.config)
@callback
def async_update_config(self, changes: dict):
"""Update existing config."""
old = self.config
new = self.config = attr.evolve(old, **changes)
self.async_schedule_save()
return attr.asdict(new)
@callback
def async_update_mode_config(self, mode: str, changes: dict):
"""Update existing config."""
modes = self.config.modes
old = (
self.config.modes[mode]
if mode in self.config.modes
else ModeEntry()
)
new = attr.evolve(old, **changes)
modes[mode] = new
self.config = attr.evolve(self.config, **{"modes": modes})
self.async_schedule_save()
return new
@callback
def async_get_area(self, area_id) -> AreaEntry:
"""Get an existing AreaEntry by id."""
res = self.areas.get(area_id)
return attr.asdict(res) if res else None
@callback
def async_get_areas(self):
"""Get an existing AreaEntry by id."""
res = {}
for (key, val) in self.areas.items():
res[key] = attr.asdict(val)
return res
@callback
def async_create_area(self, data: dict) -> AreaEntry:
"""Create a new AreaEntry."""
area_id = str(int(time.time()))
new_area = AreaEntry(**data, area_id=area_id)
self.areas[area_id] = new_area
self.async_schedule_save()
return attr.asdict(new_area)
@callback
def async_delete_area(self, area_id: str) -> None:
"""Delete AreaEntry."""
if area_id in self.areas:
del self.areas[area_id]
self.async_schedule_save()
return True
return False
@callback
def async_update_area(self, area_id: str, changes: dict) -> AreaEntry:
"""Update existing self."""
old = self.areas[area_id]
new = self.areas[area_id] = attr.evolve(old, **changes)
self.async_schedule_save()
return attr.asdict(new)
@callback
def async_get_sensor(self, entity_id) -> SensorEntry:
"""Get an existing SensorEntry by id."""
res = self.sensors.get(entity_id)
return attr.asdict(res) if res else None
@callback
def async_get_sensors(self):
"""Get an existing SensorEntry by id."""
res = {}
for (key, val) in self.sensors.items():
res[key] = attr.asdict(val)
return res
@callback
def async_create_sensor(self, entity_id: str, data: dict) -> SensorEntry:
"""Create a new SensorEntry."""
if entity_id in self.sensors:
return False
new_sensor = SensorEntry(**data, entity_id=entity_id)
self.sensors[entity_id] = new_sensor
self.async_schedule_save()
return new_sensor
@callback
def async_delete_sensor(self, entity_id: str) -> None:
"""Delete SensorEntry."""
if entity_id in self.sensors:
del self.sensors[entity_id]
self.async_schedule_save()
return True
return False
@callback
def async_update_sensor(self, entity_id: str, changes: dict) -> SensorEntry:
"""Update existing SensorEntry."""
old = self.sensors[entity_id]
new = self.sensors[entity_id] = attr.evolve(old, **changes)
self.async_schedule_save()
return new
@callback
def async_get_user(self, user_id) -> UserEntry:
"""Get an existing UserEntry by id."""
res = self.users.get(user_id)
return attr.asdict(res) if res else None
@callback
def async_get_users(self):
"""Get an existing UserEntry by id."""
res = {}
for (key, val) in self.users.items():
res[key] = attr.asdict(val)
return res
@callback
def async_create_user(self, data: dict) -> UserEntry:
"""Create a new UserEntry."""
user_id = str(int(time.time()))
new_user = UserEntry(**data, user_id=user_id)
self.users[user_id] = new_user
self.async_schedule_save()
return new_user
@callback
def async_delete_user(self, user_id: str) -> None:
"""Delete UserEntry."""
if user_id in self.users:
del self.users[user_id]
self.async_schedule_save()
return True
return False
@callback
def async_update_user(self, user_id: str, changes: dict) -> UserEntry:
"""Update existing UserEntry."""
old = self.users[user_id]
new = self.users[user_id] = attr.evolve(old, **changes)
self.async_schedule_save()
return new
@callback
def async_get_automations(self):
"""Get an existing AutomationEntry by id."""
res = {}
for (key, val) in self.automations.items():
res[key] = attr.asdict(val)
return res
@callback
def async_create_automation(self, data: dict) -> AutomationEntry:
"""Create a new AutomationEntry."""
automation_id = str(int(time.time()))
new_automation = AutomationEntry(**parse_automation_entry(data), automation_id=automation_id)
self.automations[automation_id] = new_automation
self.async_schedule_save()
return new_automation
@callback
def async_delete_automation(self, automation_id: str) -> None:
"""Delete AutomationEntry."""
if automation_id in self.automations:
del self.automations[automation_id]
self.async_schedule_save()
return True
return False
@callback
def async_update_automation(self, automation_id: str, changes: dict) -> AutomationEntry:
"""Update existing AutomationEntry."""
old = self.automations[automation_id]
new = self.automations[automation_id] = attr.evolve(old, **parse_automation_entry(changes))
self.async_schedule_save()
return new
@callback
def async_get_sensor_group(self, group_id) -> SensorGroupEntry:
"""Get an existing SensorGroupEntry by id."""
res = self.sensor_groups.get(group_id)
return attr.asdict(res) if res else None
@callback
def async_get_sensor_groups(self):
"""Get an existing SensorGroupEntry by id."""
res = {}
for (key, val) in self.sensor_groups.items():
res[key] = attr.asdict(val)
return res
@callback
def async_create_sensor_group(self, data: dict) -> SensorGroupEntry:
"""Create a new SensorGroupEntry."""
group_id = str(int(time.time()))
new_group = SensorGroupEntry(**data, group_id=group_id)
self.sensor_groups[group_id] = new_group
self.async_schedule_save()
return group_id
@callback
def async_delete_sensor_group(self, group_id: str) -> None:
"""Delete SensorGroupEntry."""
if group_id in self.sensor_groups:
del self.sensor_groups[group_id]
self.async_schedule_save()
return True
return False
@callback
def async_update_sensor_group(self, group_id: str, changes: dict) -> SensorGroupEntry:
"""Update existing SensorGroupEntry."""
old = self.sensor_groups[group_id]
new = self.sensor_groups[group_id] = attr.evolve(old, **changes)
self.async_schedule_save()
return new
@bind_hass
async def async_get_registry(hass: HomeAssistant) -> AlarmoStorage:
"""Return alarmo storage instance."""
task = hass.data.get(DATA_REGISTRY)
if task is None:
async def _load_reg() -> AlarmoStorage:
registry = AlarmoStorage(hass)
await registry.async_load()
return registry
task = hass.data[DATA_REGISTRY] = hass.async_create_task(_load_reg())
return cast(AlarmoStorage, await task)
================================================
FILE: custom_components/alarmo/websockets.py
================================================
import voluptuous as vol
import logging
from homeassistant.components import websocket_api
from homeassistant.core import callback
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.helpers import config_validation as cv
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_CODE_FORMAT,
ATTR_NAME,
ATTR_CODE,
ATTR_SERVICE,
CONF_SERVICE_DATA,
ATTR_STATE,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_VACATION,
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
STATE_ALARM_PENDING,
STATE_ALARM_DISARMING,
STATE_ALARM_ARMING,
)
from homeassistant.components.alarm_control_panel import (
CodeFormat,
ATTR_CODE_ARM_REQUIRED,
)
from homeassistant.components.websocket_api import (decorators, async_register_command)
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from . import const
from homeassistant.components.mqtt import (
DOMAIN as ATTR_MQTT,
CONF_STATE_TOPIC,
CONF_COMMAND_TOPIC,
)
import homeassistant.util.dt as dt_util
from .mqtt import (
CONF_EVENT_TOPIC,
)
from .sensors import (
ATTR_USE_EXIT_DELAY,
ATTR_USE_ENTRY_DELAY,
ATTR_ALWAYS_ON,
ATTR_ARM_ON_CLOSE,
ATTR_ALLOW_OPEN,
ATTR_TRIGGER_UNAVAILABLE,
ATTR_AUTO_BYPASS,
ATTR_AUTO_BYPASS_MODES,
ATTR_GROUP,
ATTR_GROUP_ID,
ATTR_TIMEOUT,
ATTR_EVENT_COUNT,
ATTR_ENTITIES,
SENSOR_TYPES,
)
_LOGGER = logging.getLogger(__name__)
@callback
@decorators.websocket_command({
vol.Required("type"): "alarmo_config_updated",
})
@decorators.async_response
async def handle_subscribe_updates(hass, connection, msg):
"""Handle subscribe updates."""
@callback
def async_handle_event():
"""Forward events to websocket."""
connection.send_message({
"id": msg["id"],
"type": "event",
})
connection.subscriptions[msg["id"]] = async_dispatcher_connect(
hass,
"alarmo_update_frontend",
async_handle_event
)
connection.send_result(msg["id"])
class AlarmoConfigView(HomeAssistantView):
"""Login to Home Assistant cloud."""
url = "/api/alarmo/config"
name = "api:alarmo:config"
@RequestDataValidator(
vol.Schema(
{
vol.Optional(ATTR_CODE_ARM_REQUIRED): cv.boolean,
vol.Optional(const.ATTR_CODE_DISARM_REQUIRED): cv.boolean,
vol.Optional(ATTR_CODE_FORMAT): vol.In(
[CodeFormat.NUMBER, CodeFormat.TEXT]
),
vol.Optional(const.ATTR_TRIGGER_TIME): cv.positive_int,
vol.Optional(const.ATTR_DISARM_AFTER_TRIGGER): cv.boolean,
vol.Optional(ATTR_MQTT): vol.Schema({
vol.Required(const.ATTR_ENABLED): cv.boolean,
vol.Required(CONF_STATE_TOPIC): cv.string,
vol.Optional(const.ATTR_STATE_PAYLOAD): vol.Schema({
vol.Optional(STATE_ALARM_DISARMED): cv.string,
vol.Optional(STATE_ALARM_ARMED_HOME): cv.string,
vol.Optional(STATE_ALARM_ARMED_AWAY): cv.string,
vol.Optional(STATE_ALARM_ARMED_NIGHT): cv.string,
vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS): cv.string,
vol.Optional(STATE_ALARM_ARMED_VACATION): cv.string,
vol.Optional(STATE_ALARM_PENDING): cv.string,
vol.Optional(STATE_ALARM_ARMING): cv.string,
vol.Optional(STATE_ALARM_DISARMING): cv.string,
vol.Optional(STATE_ALARM_TRIGGERED): cv.string
}),
vol.Required(CONF_COMMAND_TOPIC): cv.string,
vol.Optional(const.ATTR_COMMAND_PAYLOAD): vol.Schema({
vol.Optional(const.COMMAND_ARM_AWAY): cv.string,
vol.Optional(const.COMMAND_ARM_HOME): cv.string,
vol.Optional(const.COMMAND_ARM_NIGHT): cv.string,
vol.Optional(const.COMMAND_ARM_CUSTOM_BYPASS): cv.string,
vol.Optional(const.COMMAND_ARM_VACATION): cv.string,
vol.Optional(const.COMMAND_DISARM): cv.string,
}),
vol.Required(const.ATTR_REQUIRE_CODE): cv.boolean,
vol.Required(CONF_EVENT_TOPIC): cv.string,
}),
vol.Optional(const.ATTR_MASTER): vol.Schema({
vol.Required(const.ATTR_ENABLED): cv.boolean,
vol.Optional(ATTR_NAME): cv.string,
})
}
)
)
async def post(self, request, data):
"""Handle config update request."""
hass = request.app["hass"]
coordinator = hass.data[const.DOMAIN]["coordinator"]
await coordinator.async_update_config(data)
async_dispatcher_send(hass, "alarmo_update_frontend")
return self.json({"success": True})
class AlarmoAreaView(HomeAssistantView):
"""Login to Home Assistant cloud."""
url = "/api/alarmo/area"
name = "api:alarmo:area"
mode_schema = vol.Schema({
vol.Required(const.ATTR_ENABLED): cv.boolean,
vol.Required(const.ATTR_EXIT_TIME): cv.positive_int,
vol.Required(const.ATTR_ENTRY_TIME): cv.positive_int,
vol.Optional(const.ATTR_TRIGGER_TIME): cv.positive_int,
})
@RequestDataValidator(
vol.Schema(
{
vol.Optional("area_id"): cv.string,
vol.Optional(ATTR_NAME): cv.string,
vol.Optional(const.ATTR_REMOVE): cv.boolean,
vol.Optional(const.ATTR_MODES): vol.Schema({
vol.Optional(STATE_ALARM_ARMED_AWAY): mode_schema,
vol.Optional(STATE_ALARM_ARMED_HOME): mode_schema,
vol.Optional(STATE_ALARM_ARMED_NIGHT): mode_schema,
vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS): mode_schema,
vol.Optional(STATE_ALARM_ARMED_VACATION): mode_schema
})
}
)
)
async def post(self, request, data):
"""Handle config update request."""
hass = request.app["hass"]
coordinator = hass.data[const.DOMAIN]["coordinator"]
if "area_id" in data:
area = data["area_id"]
del data["area_id"]
else:
area = None
await coordinator.async_update_area_config(area, data)
async_dispatcher_send(hass, "alarmo_update_frontend")
return self.json({"success": True})
class AlarmoSensorView(HomeAssistantView):
"""Login to Home Assistant cloud."""
url = "/api/alarmo/sensors"
name = "api:alarmo:sensors"
@RequestDataValidator(
vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
vol.Optional(const.ATTR_REMOVE): cv.boolean,
vol.Optional(const.ATTR_TYPE): vol.In(SENSOR_TYPES),
vol.Optional(const.ATTR_MODES): vol.All(
cv.ensure_list,
[vol.In(const.ARM_MODES)]
),
vol.Optional(ATTR_USE_EXIT_DELAY): cv.boolean,
vol.Optional(ATTR_USE_ENTRY_DELAY): cv.boolean,
vol.Optional(ATTR_ARM_ON_CLOSE): cv.boolean,
vol.Optional(ATTR_ALLOW_OPEN): cv.boolean,
vol.Optional(ATTR_ALWAYS_ON): cv.boolean,
vol.Optional(ATTR_TRIGGER_UNAVAILABLE): cv.boolean,
vol.Optional(ATTR_AUTO_BYPASS): cv.boolean,
vol.Optional(ATTR_AUTO_BYPASS_MODES): vol.All(
cv.ensure_list,
[vol.In(const.ARM_MODES)]
),
vol.Optional(const.ATTR_AREA): cv.string,
vol.Optional(const.ATTR_ENABLED): cv.boolean,
vol.Optional(ATTR_GROUP): vol.Any(
cv.string,
None
)
}
)
)
async def post(self, request, data):
"""Handle config update request."""
hass = request.app["hass"]
coordinator = hass.data[const.DOMAIN]["coordinator"]
entity = data[ATTR_ENTITY_ID]
del data[ATTR_ENTITY_ID]
coordinator.async_update_sensor_config(entity, data)
async_dispatcher_send(hass, "alarmo_update_frontend")
return self.json({"success": True})
class AlarmoUserView(HomeAssistantView):
"""Login to Home Assistant cloud."""
url = "/api/alarmo/users"
name = "api:alarmo:users"
@RequestDataValidator(
vol.Schema(
{
vol.Optional(const.ATTR_USER_ID): cv.string,
vol.Optional(const.ATTR_REMOVE): cv.boolean,
vol.Optional(ATTR_NAME): cv.string,
vol.Optional(const.ATTR_ENABLED): cv.boolean,
vol.Optional(ATTR_CODE): cv.string,
vol.Optional(const.ATTR_OLD_CODE): cv.string,
vol.Optional(const.ATTR_CAN_ARM): cv.boolean,
vol.Optional(const.ATTR_CAN_DISARM): cv.boolean,
vol.Optional(const.ATTR_IS_OVERRIDE_CODE): cv.boolean,
vol.Optional(const.ATTR_AREA_LIMIT): vol.All(
cv.ensure_list,
[cv.string]
)
}
)
)
async def post(self, request, data):
"""Handle config update request."""
hass = request.app["hass"]
coordinator = hass.data[const.DOMAIN]["coordinator"]
user_id = None
if const.ATTR_USER_ID in data:
user_id = data[const.ATTR_USER_ID]
del data[const.ATTR_USER_ID]
coordinator.async_update_user_config(user_id, data)
async_dispatcher_send(hass, "alarmo_update_frontend")
return self.json({"success": True})
class AlarmoAutomationView(HomeAssistantView):
"""Login to Home Assistant cloud."""
url = "/api/alarmo/automations"
name = "api:alarmo:automations"
@RequestDataValidator(
vol.Schema(
{
vol.Optional(const.ATTR_AUTOMATION_ID): cv.string,
vol.Optional(ATTR_NAME): cv.string,
vol.Optional(const.ATTR_TYPE): cv.string,
vol.Optional(const.ATTR_TRIGGERS): vol.All(
cv.ensure_list,
[vol.Any(
vol.Schema(
{
vol.Required(const.ATTR_EVENT): cv.string,
vol.Optional(const.ATTR_AREA): vol.Any(
int,
cv.string,
),
vol.Optional(const.ATTR_MODES): vol.All(
cv.ensure_list,
[vol.In(const.ARM_MODES)]
),
}
),
vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.string,
vol.Required(ATTR_STATE): cv.string,
}
)
)]
),
vol.Optional(const.ATTR_ACTIONS): vol.All(
cv.ensure_list,
[vol.Schema(
{
vol.Optional(ATTR_ENTITY_ID): cv.string,
vol.Required(ATTR_SERVICE): cv.string,
vol.Optional(CONF_SERVICE_DATA): dict,
}
)]
),
vol.Optional(const.ATTR_ENABLED): cv.boolean,
vol.Optional(const.ATTR_REMOVE): cv.boolean,
}
)
)
async def post(self, request, data):
"""Handle config update request."""
hass = request.app["hass"]
coordinator = hass.data[const.DOMAIN]["coordinator"]
automation_id = None
if const.ATTR_AUTOMATION_ID in data:
automation_id = data[const.ATTR_AUTOMATION_ID]
del data[const.ATTR_AUTOMATION_ID]
coordinator.async_update_automation_config(automation_id, data)
async_dispatcher_send(hass, "alarmo_update_frontend")
return self.json({"success": True})
class AlarmoSensorGroupView(HomeAssistantView):
"""Login to Home Assistant cloud."""
url = "/api/alarmo/sensor_groups"
name = "api:alarmo:sensor_groups"
@RequestDataValidator(
vol.Schema(
{
vol.Optional(ATTR_GROUP_ID): cv.string,
vol.Optional(ATTR_NAME): cv.string,
vol.Optional(ATTR_ENTITIES): vol.All(
cv.ensure_list,
vol.Unique(),
[cv.string]
),
vol.Optional(ATTR_TIMEOUT): cv.positive_int,
vol.Optional(ATTR_EVENT_COUNT): cv.positive_int,
vol.Optional(const.ATTR_REMOVE): cv.boolean,
}
)
)
async def post(self, request, data):
"""Handle config update request."""
hass = request.app["hass"]
coordinator = hass.data[const.DOMAIN]["coordinator"]
group_id = None
if ATTR_GROUP_ID in data:
group_id = data[ATTR_GROUP_ID]
del data[ATTR_GROUP_ID]
coordinator.async_update_sensor_group_config(group_id, data)
async_dispatcher_send(hass, "alarmo_update_frontend")
return self.json({"success": True})
@callback
def websocket_get_config(hass, connection, msg):
"""Publish config data."""
coordinator = hass.data[const.DOMAIN]["coordinator"]
config = coordinator.store.async_get_config()
connection.send_result(msg["id"], config)
@callback
def websocket_get_areas(hass, connection, msg):
"""Publish area data."""
coordinator = hass.data[const.DOMAIN]["coordinator"]
areas = coordinator.store.async_get_areas()
connection.send_result(msg["id"], areas)
@callback
def websocket_get_sensors(hass, connection, msg):
"""Publish sensor data."""
coordinator = hass.data[const.DOMAIN]["coordinator"]
sensors = coordinator.store.async_get_sensors()
for entity_id in sensors.keys():
group = coordinator.async_get_group_for_sensor(entity_id)
sensors[entity_id]["group"] = group
connection.send_result(msg["id"], sensors)
@callback
def websocket_get_users(hass, connection, msg):
"""Publish user data."""
coordinator = hass.data[const.DOMAIN]["coordinator"]
users = coordinator.store.async_get_users()
connection.send_result(msg["id"], users)
@callback
def websocket_get_automations(hass, connection, msg):
"""Publish automations data."""
coordinator = hass.data[const.DOMAIN]["coordinator"]
automations = coordinator.store.async_get_automations()
connection.send_result(msg["id"], automations)
@callback
def websocket_get_alarm_entities(hass, connection, msg):
"""Publish alarm entity data."""
result = [
{
"entity_id": entity.entity_id,
"area_id": area_id
}
for (area_id, entity) in hass.data[const.DOMAIN]["areas"].items()
]
if hass.data[const.DOMAIN]["master"]:
result.append({
"entity_id": hass.data[const.DOMAIN]["master"].entity_id,
"area_id": 0
})
connection.send_result(msg["id"], result)
@callback
def websocket_get_sensor_groups(hass, connection, msg):
"""Publish sensor_group data."""
coordinator = hass.data[const.DOMAIN]["coordinator"]
groups = coordinator.store.async_get_sensor_groups()
connection.send_result(msg["id"], groups)
@callback
def websocket_get_countdown(hass, connection, msg):
"""Publish countdown time for alarm entity."""
entity_id = msg["entity_id"]
item = next((entity for entity in hass.data[const.DOMAIN]["areas"].values() if entity.entity_id == entity_id), None)
if hass.data[const.DOMAIN]["master"] and not item and hass.data[const.DOMAIN]["master"].entity_id == entity_id:
item = hass.data[const.DOMAIN]["master"]
data = {
"delay": item.delay if item else 0,
"remaining": round((item.expiration - dt_util.utcnow()).total_seconds(),2) if item and item.expiration else 0
}
connection.send_result(msg["id"], data)
async def async_register_websockets(hass):
hass.http.register_view(AlarmoConfigView)
hass.http.register_view(AlarmoSensorView)
hass.http.register_view(AlarmoUserView)
hass.http.register_view(AlarmoAutomationView)
hass.http.register_view(AlarmoAreaView)
hass.http.register_view(AlarmoSensorGroupView)
async_register_command(
hass,
handle_subscribe_updates
)
async_register_command(
hass,
"alarmo/config",
websocket_get_config,
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{vol.Required("type"): "alarmo/config"}
),
)
async_register_command(
hass,
"alarmo/areas",
websocket_get_areas,
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{vol.Required("type"): "alarmo/areas"}
),
)
async_register_command(
hass,
"alarmo/sensors",
websocket_get_sensors,
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{vol.Required("type"): "alarmo/sensors"}
),
)
async_register_command(
hass,
"alarmo/users",
websocket_get_users,
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{vol.Required("type"): "alarmo/users"}
),
)
async_register_command(
hass,
"alarmo/automations",
websocket_get_automations,
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{vol.Required("type"): "alarmo/automations"}
),
)
async_register_command(
hass,
"alarmo/entities",
websocket_get_alarm_entities,
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{vol.Required("type"): "alarmo/entities"}
),
)
async_register_command(
hass,
"alarmo/sensor_groups",
websocket_get_sensor_groups,
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{vol.Required("type"): "alarmo/sensor_groups"}
),
)
async_register_command(
hass,
"alarmo/countdown",
websocket_get_countdown,
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{
vol.Required("type"): "alarmo/countdown",
vol.Required("entity_id"): cv.entity_id
}
),
)
================================================
FILE: custom_components/hacs/__init__.py
================================================
"""
HACS gives you a powerful UI to handle downloads of all your custom needs.
For more details about this integration, please refer to the documentation at
https://hacs.xyz/
"""
from __future__ import annotations
import os
from typing import Any
from aiogithubapi import AIOGitHubAPIException, GitHub, GitHubAPI
from aiogithubapi.const import ACCEPT_HEADERS
from awesomeversion import AwesomeVersion
from homeassistant.components.lovelace.system_health import system_health_info
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import Platform, __version__ as HAVERSION
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.start import async_at_start
from homeassistant.loader import async_get_integration
import voluptuous as vol
from .base import HacsBase
from .const import DOMAIN, MINIMUM_HA_VERSION, STARTUP
from .enums import ConfigurationType, HacsDisabledReason, HacsStage, LovelaceMode
from .frontend import async_register_frontend
from .utils.configuration_schema import hacs_config_combined
from .utils.data import HacsData
from .utils.platform_setup import async_setup_entity_platforms
from .utils.queue_manager import QueueManager
from .utils.version import version_left_higher_or_equal_then_right
from .websocket import async_register_websocket_commands
CONFIG_SCHEMA = vol.Schema({DOMAIN: hacs_config_combined()}, extra=vol.ALLOW_EXTRA)
async def async_initialize_integration(
hass: HomeAssistant,
*,
config_entry: ConfigEntry | None = None,
config: dict[str, Any] | None = None,
) -> bool:
"""Initialize the integration"""
hass.data[DOMAIN] = hacs = HacsBase()
hacs.enable_hacs()
if config is not None:
if DOMAIN not in config:
return True
if hacs.configuration.config_type == ConfigurationType.CONFIG_ENTRY:
return True
hacs.configuration.update_from_dict(
{
"config_type": ConfigurationType.YAML,
**config[DOMAIN],
"config": config[DOMAIN],
}
)
if config_entry is not None:
if config_entry.source == SOURCE_IMPORT:
hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id))
return False
hacs.configuration.update_from_dict(
{
"config_entry": config_entry,
"config_type": ConfigurationType.CONFIG_ENTRY,
**config_entry.data,
**config_entry.options,
}
)
integration = await async_get_integration(hass, DOMAIN)
hacs.set_stage(None)
hacs.log.info(STARTUP, integration.version)
clientsession = async_get_clientsession(hass)
hacs.integration = integration
hacs.version = integration.version
hacs.configuration.dev = integration.version == "0.0.0"
hacs.hass = hass
hacs.queue = QueueManager(hass=hass)
hacs.data = HacsData(hacs=hacs)
hacs.system.running = True
hacs.session = clientsession
hacs.core.lovelace_mode = LovelaceMode.YAML
try:
lovelace_info = await system_health_info(hacs.hass)
hacs.core.lovelace_mode = LovelaceMode(lovelace_info.get("mode", "yaml"))
except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except
# If this happens, the users YAML is not valid, we assume YAML mode
pass
hacs.log.debug("Configuration type: %s", hacs.configuration.config_type)
hacs.core.config_path = hacs.hass.config.path()
if hacs.core.ha_version is None:
hacs.core.ha_version = AwesomeVersion(HAVERSION)
## Legacy GitHub client
hacs.github = GitHub(
hacs.configuration.token,
clientsession,
headers={
"User-Agent": f"HACS/{hacs.version}",
"Accept": ACCEPT_HEADERS["preview"],
},
)
## New GitHub client
hacs.githubapi = GitHubAPI(
token=hacs.configuration.token,
session=clientsession,
**{"client_name": f"HACS/{hacs.version}"},
)
async def async_startup():
"""HACS startup tasks."""
hacs.enable_hacs()
for location in (
hass.config.path("custom_components/custom_updater.py"),
hass.config.path("custom_components/custom_updater/__init__.py"),
):
if os.path.exists(location):
hacs.log.critical(
"This cannot be used with custom_updater. "
"To use this you need to remove custom_updater form %s",
location,
)
hacs.disable_hacs(HacsDisabledReason.CONSTRAINS)
return False
if not version_left_higher_or_equal_then_right(
hacs.core.ha_version.string,
MINIMUM_HA_VERSION,
):
hacs.log.critical(
"You need HA version %s or newer to use this integration.",
MINIMUM_HA_VERSION,
)
hacs.disable_hacs(HacsDisabledReason.CONSTRAINS)
return False
if not await hacs.data.restore():
hacs.disable_hacs(HacsDisabledReason.RESTORE)
return False
can_update = await hacs.async_can_update()
hacs.log.debug("Can update %s repositories", can_update)
hacs.set_active_categories()
async_register_websocket_commands(hass)
async_register_frontend(hass, hacs)
if hacs.configuration.config_type == ConfigurationType.YAML:
hass.async_create_task(
async_load_platform(hass, Platform.SENSOR, DOMAIN, {}, hacs.configuration.config)
)
hacs.log.info("Update entities are only supported when using UI configuration")
else:
await async_setup_entity_platforms(
hacs,
hass,
config_entry,
[Platform.SENSOR, Platform.UPDATE]
if hacs.configuration.experimental
else [Platform.SENSOR],
)
hacs.set_stage(HacsStage.SETUP)
if hacs.system.disabled:
return False
# Schedule startup tasks
async_at_start(hass=hass, at_start_cb=hacs.startup_tasks)
hacs.set_stage(HacsStage.WAITING)
hacs.log.info("Setup complete, waiting for Home Assistant before startup tasks starts")
return not hacs.system.disabled
async def async_try_startup(_=None):
"""Startup wrapper for yaml config."""
try:
startup_result = await async_startup()
except AIOGitHubAPIException:
startup_result = False
if not startup_result:
if (
hacs.configuration.config_type == ConfigurationType.YAML
or hacs.system.disabled_reason != HacsDisabledReason.INVALID_TOKEN
):
hacs.log.info("Could not setup HACS, trying again in 15 min")
async_call_later(hass, 900, async_try_startup)
return
hacs.enable_hacs()
await async_try_startup()
# Mischief managed!
return True
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
"""Set up this integration using yaml."""
return await async_initialize_integration(hass=hass, config=config)
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up this integration using UI."""
config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry))
setup_result = await async_initialize_integration(hass=hass, config_entry=config_entry)
hacs: HacsBase = hass.data[DOMAIN]
return setup_result and not hacs.system.disabled
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Handle removal of an entry."""
hacs: HacsBase = hass.data[DOMAIN]
# Clear out pending queue
hacs.queue.clear()
for task in hacs.recuring_tasks:
# Cancel all pending tasks
task()
# Store data
await hacs.data.async_write(force=True)
try:
if hass.data.get("frontend_panels", {}).get("hacs"):
hacs.log.info("Removing sidepanel")
hass.components.frontend.async_remove_panel("hacs")
except AttributeError:
pass
platforms = ["sensor"]
if hacs.configuration.experimental:
platforms.append("update")
unload_ok = await hass.config_entries.async_unload_platforms(config_entry, platforms)
hacs.set_stage(None)
hacs.disable_hacs(HacsDisabledReason.REMOVED)
hass.data.pop(DOMAIN, None)
return unload_ok
async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Reload the HACS config entry."""
await async_unload_entry(hass, config_entry)
await async_setup_entry(hass, config_entry)
================================================
FILE: custom_components/hacs/base.py
================================================
"""Base HACS class."""
from __future__ import annotations
import asyncio
from dataclasses import asdict, dataclass, field
from datetime import timedelta
import gzip
import logging
import math
import os
import pathlib
import shutil
from typing import TYPE_CHECKING, Any, Awaitable, Callable
from aiogithubapi import (
AIOGitHubAPIException,
GitHub,
GitHubAPI,
GitHubAuthenticationException,
GitHubException,
GitHubNotModifiedException,
GitHubRatelimitException,
)
from aiogithubapi.objects.repository import AIOGitHubAPIRepository
from aiohttp.client import ClientSession, ClientTimeout
from awesomeversion import AwesomeVersion
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.loader import Integration
from homeassistant.util import dt
from .const import TV
from .enums import (
ConfigurationType,
HacsCategory,
HacsDisabledReason,
HacsDispatchEvent,
HacsGitHubRepo,
HacsStage,
LovelaceMode,
)
from .exceptions import (
AddonRepositoryException,
HacsException,
HacsExecutionStillInProgress,
HacsExpectedException,
HacsRepositoryArchivedException,
HacsRepositoryExistException,
HomeAssistantCoreRepositoryException,
)
from .repositories import RERPOSITORY_CLASSES
from .utils.decode import decode_content
from .utils.json import json_loads
from .utils.logger import LOGGER
from .utils.platform_setup import async_setup_entity_platforms
from .utils.queue_manager import QueueManager
from .utils.store import async_load_from_store, async_save_to_store
if TYPE_CHECKING:
from .repositories.base import HacsRepository
from .utils.data import HacsData
from .validate.manager import ValidationManager
@dataclass
class RemovedRepository:
"""Removed repository."""
repository: str | None = None
reason: str | None = None
link: str | None = None
removal_type: str = None # archived, not_compliant, critical, dev, broken
acknowledged: bool = False
def update_data(self, data: dict):
"""Update data of the repository."""
for key in data:
if data[key] is None:
continue
if key in (
"reason",
"link",
"removal_type",
"acknowledged",
):
self.__setattr__(key, data[key])
def to_json(self):
"""Return a JSON representation of the data."""
return {
"repository": self.repository,
"reason": self.reason,
"link": self.link,
"removal_type": self.removal_type,
"acknowledged": self.acknowledged,
}
@dataclass
class HacsConfiguration:
"""HacsConfiguration class."""
appdaemon_path: str = "appdaemon/apps/"
appdaemon: bool = False
config: dict[str, Any] = field(default_factory=dict)
config_entry: ConfigEntry | None = None
config_type: ConfigurationType | None = None
country: str = "ALL"
debug: bool = False
dev: bool = False
experimental: bool = False
frontend_repo_url: str = ""
frontend_repo: str = ""
netdaemon_path: str = "netdaemon/apps/"
netdaemon: bool = False
plugin_path: str = "www/community/"
python_script_path: str = "python_scripts/"
python_script: bool = False
release_limit: int = 5
sidepanel_icon: str = "hacs:hacs"
sidepanel_title: str = "HACS"
theme_path: str = "themes/"
theme: bool = False
token: str = None
def to_json(self) -> str:
"""Return a json string."""
return asdict(self)
def update_from_dict(self, data: dict) -> None:
"""Set attributes from dicts."""
if not isinstance(data, dict):
raise HacsException("Configuration is not valid.")
for key in data:
self.__setattr__(key, data[key])
@dataclass
class HacsCore:
"""HACS Core info."""
config_path: pathlib.Path | None = None
ha_version: AwesomeVersion | None = None
lovelace_mode = LovelaceMode("yaml")
@dataclass
class HacsCommon:
"""Common for HACS."""
categories: set[str] = field(default_factory=set)
renamed_repositories: dict[str, str] = field(default_factory=dict)
archived_repositories: list[str] = field(default_factory=list)
ignored_repositories: list[str] = field(default_factory=list)
skip: list[str] = field(default_factory=list)
@dataclass
class HacsStatus:
"""HacsStatus."""
startup: bool = True
new: bool = False
@dataclass
class HacsSystem:
"""HACS System info."""
disabled_reason: HacsDisabledReason | None = None
running: bool = False
stage = HacsStage.SETUP
action: bool = False
@property
def disabled(self) -> bool:
"""Return if HACS is disabled."""
return self.disabled_reason is not None
@dataclass
class HacsRepositories:
"""HACS Repositories."""
_default_repositories: set[str] = field(default_factory=set)
_repositories: list[HacsRepository] = field(default_factory=list)
_repositories_by_full_name: dict[str, str] = field(default_factory=dict)
_repositories_by_id: dict[str, str] = field(default_factory=dict)
_removed_repositories: list[RemovedRepository] = field(default_factory=list)
@property
def list_all(self) -> list[HacsRepository]:
"""Return a list of repositories."""
return self._repositories
@property
def list_removed(self) -> list[RemovedRepository]:
"""Return a list of removed repositories."""
return self._removed_repositories
@property
def list_downloaded(self) -> list[HacsRepository]:
"""Return a list of downloaded repositories."""
return [repo for repo in self._repositories if repo.data.installed]
def register(self, repository: HacsRepository, default: bool = False) -> None:
"""Register a repository."""
repo_id = str(repository.data.id)
if repo_id == "0":
return
if self.is_registered(repository_id=repo_id):
return
if repository not in self._repositories:
self._repositories.append(repository)
self._repositories_by_id[repo_id] = repository
self._repositories_by_full_name[repository.data.full_name_lower] = repository
if default:
self.mark_default(repository)
def unregister(self, repository: HacsRepository) -> None:
"""Unregister a repository."""
repo_id = str(repository.data.id)
if repo_id == "0":
return
if not self.is_registered(repository_id=repo_id):
return
if self.is_default(repo_id):
self._default_repositories.remove(repo_id)
if repository in self._repositories:
self._repositories.remove(repository)
self._repositories_by_id.pop(repo_id, None)
self._repositories_by_full_name.pop(repository.data.full_name_lower, None)
def mark_default(self, repository: HacsRepository) -> None:
"""Mark a repository as default."""
repo_id = str(repository.data.id)
if repo_id == "0":
return
if not self.is_registered(repository_id=repo_id):
return
self._default_repositories.add(repo_id)
def set_repository_id(self, repository, repo_id):
"""Update a repository id."""
existing_repo_id = str(repository.data.id)
if existing_repo_id == repo_id:
return
if existing_repo_id != "0":
raise ValueError(
f"The repo id for {repository.data.full_name_lower} "
f"is already set to {existing_repo_id}"
)
repository.data.id = repo_id
self.register(repository)
def is_default(self, repository_id: str | None = None) -> bool:
"""Check if a repository is default."""
if not repository_id:
return False
return repository_id in self._default_repositories
def is_registered(
self,
repository_id: str | None = None,
repository_full_name: str | None = None,
) -> bool:
"""Check if a repository is registered."""
if repository_id is not None:
return repository_id in self._repositories_by_id
if repository_full_name is not None:
return repository_full_name in self._repositories_by_full_name
return False
def is_downloaded(
self,
repository_id: str | None = None,
repository_full_name: str | None = None,
) -> bool:
"""Check if a repository is registered."""
if repository_id is not None:
repo = self.get_by_id(repository_id)
if repository_full_name is not None:
repo = self.get_by_full_name(repository_full_name)
if repo is None:
return False
return repo.data.installed
def get_by_id(self, repository_id: str | None) -> HacsRepository | None:
"""Get repository by id."""
if not repository_id:
return None
return self._repositories_by_id.get(str(repository_id))
def get_by_full_name(self, repository_full_name: str | None) -> HacsRepository | None:
"""Get repository by full name."""
if not repository_full_name:
return None
return self._repositories_by_full_name.get(repository_full_name.lower())
def is_removed(self, repository_full_name: str) -> bool:
"""Check if a repository is removed."""
return repository_full_name in (
repository.repository for repository in self._removed_repositories
)
def removed_repository(self, repository_full_name: str) -> RemovedRepository:
"""Get repository by full name."""
if self.is_removed(repository_full_name):
if removed := [
repository
for repository in self._removed_repositories
if repository.repository == repository_full_name
]:
return removed[0]
removed = RemovedRepository(repository=repository_full_name)
self._removed_repositories.append(removed)
return removed
class HacsBase:
"""Base HACS class."""
common = HacsCommon()
configuration = HacsConfiguration()
core = HacsCore()
data: HacsData | None = None
frontend_version: str | None = None
github: GitHub | None = None
githubapi: GitHubAPI | None = None
hass: HomeAssistant | None = None
integration: Integration | None = None
log: logging.Logger = LOGGER
queue: QueueManager | None = None
recuring_tasks = []
repositories: HacsRepositories = HacsRepositories()
repository: AIOGitHubAPIRepository | None = None
session: ClientSession | None = None
stage: HacsStage | None = None
status = HacsStatus()
system = HacsSystem()
validation: ValidationManager | None = None
version: str | None = None
@property
def integration_dir(self) -> pathlib.Path:
"""Return the HACS integration dir."""
return self.integration.file_path
def set_stage(self, stage: HacsStage | None) -> None:
"""Set HACS stage."""
if stage and self.stage == stage:
return
self.stage = stage
if stage is not None:
self.log.info("Stage changed: %s", self.stage)
self.async_dispatch(HacsDispatchEvent.STAGE, {"stage": self.stage})
def disable_hacs(self, reason: HacsDisabledReason) -> None:
"""Disable HACS."""
if self.system.disabled_reason == reason:
return
self.system.disabled_reason = reason
if reason != HacsDisabledReason.REMOVED:
self.log.error("HACS is disabled - %s", reason)
if (
reason == HacsDisabledReason.INVALID_TOKEN
and self.configuration.config_type == ConfigurationType.CONFIG_ENTRY
):
self.configuration.config_entry.state = ConfigEntryState.SETUP_ERROR
self.configuration.config_entry.reason = "Authentication failed"
self.hass.add_job(self.configuration.config_entry.async_start_reauth, self.hass)
def enable_hacs(self) -> None:
"""Enable HACS."""
if self.system.disabled_reason is not None:
self.system.disabled_reason = None
self.log.info("HACS is enabled")
def enable_hacs_category(self, category: HacsCategory) -> None:
"""Enable HACS category."""
if category not in self.common.categories:
self.log.info("Enable category: %s", category)
self.common.categories.add(category)
def disable_hacs_category(self, category: HacsCategory) -> None:
"""Disable HACS category."""
if category in self.common.categories:
self.log.info("Disabling category: %s", category)
self.common.categories.pop(category)
async def async_save_file(self, file_path: str, content: Any) -> bool:
"""Save a file."""
def _write_file():
with open(
file_path,
mode="w" if isinstance(content, str) else "wb",
encoding="utf-8" if isinstance(content, str) else None,
errors="ignore" if isinstance(content, str) else None,
) as file_handler:
file_handler.write(content)
# Create gz for .js files
if os.path.isfile(file_path):
if file_path.endswith(".js"):
with open(file_path, "rb") as f_in:
with gzip.open(file_path + ".gz", "wb") as f_out:
shutil.copyfileobj(f_in, f_out)
# LEGACY! Remove with 2.0
if "themes" in file_path and file_path.endswith(".yaml"):
filename = file_path.split("/")[-1]
base = file_path.split("/themes/")[0]
combined = f"{base}/themes/{filename}"
if os.path.exists(combined):
self.log.info("Removing old theme file %s", combined)
os.remove(combined)
try:
await self.hass.async_add_executor_job(_write_file)
except BaseException as error: # lgtm [py/catch-base-exception] pylint: disable=broad-except
self.log.error("Could not write data to %s - %s", file_path, error)
return False
return os.path.exists(file_path)
async def async_can_update(self) -> int:
"""Helper to calculate the number of repositories we can fetch data for."""
try:
response = await self.async_github_api_method(self.githubapi.rate_limit)
if ((limit := response.data.resources.core.remaining or 0) - 1000) >= 10:
return math.floor((limit - 1000) / 10)
reset = dt.as_local(dt.utc_from_timestamp(response.data.resources.core.reset))
self.log.info(
"GitHub API ratelimited - %s remaining (%s)",
response.data.resources.core.remaining,
f"{reset.hour}:{reset.minute}:{reset.second}",
)
self.disable_hacs(HacsDisabledReason.RATE_LIMIT)
except BaseException as exception: # lgtm [py/catch-base-exception] pylint: disable=broad-except
self.log.exception(exception)
return 0
async def async_github_get_hacs_default_file(self, filename: str) -> list:
"""Get the content of a default file."""
response = await self.async_github_api_method(
method=self.githubapi.repos.contents.get,
repository=HacsGitHubRepo.DEFAULT,
path=filename,
)
if response is None:
return []
return json_loads(decode_content(response.data.content))
async def async_github_api_method(
self,
method: Callable[[], Awaitable[TV]],
*args,
raise_exception: bool = True,
**kwargs,
) -> TV | None:
"""Call a GitHub API method"""
_exception = None
try:
return await method(*args, **kwargs)
except GitHubAuthenticationException as exception:
self.disable_hacs(HacsDisabledReason.INVALID_TOKEN)
_exception = exception
except GitHubRatelimitException as exception:
self.disable_hacs(HacsDisabledReason.RATE_LIMIT)
_exception = exception
except GitHubNotModifiedException as exception:
raise exception
except GitHubException as exception:
_exception = exception
except BaseException as exception: # lgtm [py/catch-base-exception] pylint: disable=broad-except
self.log.exception(exception)
_exception = exception
if raise_exception and _exception is not None:
raise HacsException(_exception)
return None
async def async_register_repository(
self,
repository_full_name: str,
category: HacsCategory,
*,
check: bool = True,
ref: str | None = None,
repository_id: str | None = None,
default: bool = False,
) -> None:
"""Register a repository."""
if repository_full_name in self.common.skip:
if repository_full_name != HacsGitHubRepo.INTEGRATION:
raise HacsExpectedException(f"Skipping {repository_full_name}")
if repository_full_name == "home-assistant/core":
raise HomeAssistantCoreRepositoryException()
if repository_full_name == "home-assistant/addons" or repository_full_name.startswith(
"hassio-addons/"
):
raise AddonRepositoryException()
if category not in RERPOSITORY_CLASSES:
raise HacsException(f"{category} is not a valid repository category.")
if (renamed := self.common.renamed_repositories.get(repository_full_name)) is not None:
repository_full_name = renamed
repository: HacsRepository = RERPOSITORY_CLASSES[category](self, repository_full_name)
if check:
try:
await repository.async_registration(ref)
if self.status.new:
repository.data.new = False
if repository.validate.errors:
self.common.skip.append(repository.data.full_name)
if not self.status.startup:
self.log.error("Validation for %s failed.", repository_full_name)
if self.system.action:
raise HacsException(
f"::error:: Validation for {repository_full_name} failed."
)
return repository.validate.errors
if self.system.action:
repository.logger.info("%s Validation completed", repository.string)
else:
repository.logger.info("%s Registration completed", repository.string)
except (HacsRepositoryExistException, HacsRepositoryArchivedException):
return
except AIOGitHubAPIException as exception:
self.common.skip.append(repository.data.full_name)
raise HacsException(
f"Validation for {repository_full_name} failed with {exception}."
) from exception
if repository_id is not None:
repository.data.id = repository_id
if str(repository.data.id) != "0" and (
exists := self.repositories.get_by_id(repository.data.id)
):
self.repositories.unregister(exists)
else:
if self.hass is not None and ((check and repository.data.new) or self.status.new):
self.async_dispatch(
HacsDispatchEvent.REPOSITORY,
{
"action": "registration",
"repository": repository.data.full_name,
"repository_id": repository.data.id,
},
)
self.repositories.register(repository, default)
async def startup_tasks(self, _=None) -> None:
"""Tasks that are started after setup."""
self.set_stage(HacsStage.STARTUP)
try:
repository = self.repositories.get_by_full_name(HacsGitHubRepo.INTEGRATION)
if repository is None:
await self.async_register_repository(
repository_full_name=HacsGitHubRepo.INTEGRATION,
category=HacsCategory.INTEGRATION,
default=True,
)
repository = self.repositories.get_by_full_name(HacsGitHubRepo.INTEGRATION)
if repository is None:
raise HacsException("Unknown error")
repository.data.installed = True
repository.data.installed_version = self.integration.version.string
repository.data.new = False
repository.data.releases = True
self.repository = repository.repository_object
self.repositories.mark_default(repository)
except HacsException as exception:
if "403" in str(exception):
self.log.critical(
"GitHub API is ratelimited, or the token is wrong.",
)
else:
self.log.critical("Could not load HACS! - %s", exception)
self.disable_hacs(HacsDisabledReason.LOAD_HACS)
if critical := await async_load_from_store(self.hass, "critical"):
for repo in critical:
if not repo["acknowledged"]:
self.log.critical("URGENT!: Check the HACS panel!")
self.hass.components.persistent_notification.create(
title="URGENT!", message="**Check the HACS panel!**"
)
break
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_get_all_category_repositories, timedelta(hours=3)
)
)
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_update_all_repositories, timedelta(hours=25)
)
)
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_check_rate_limit, timedelta(minutes=5)
)
)
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_prosess_queue, timedelta(minutes=10)
)
)
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_update_downloaded_repositories, timedelta(hours=2)
)
)
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_handle_critical_repositories, timedelta(hours=2)
)
)
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_FINAL_WRITE, self.data.async_force_write
)
self.status.startup = False
self.async_dispatch(HacsDispatchEvent.STATUS, {})
await self.async_handle_removed_repositories()
await self.async_get_all_category_repositories()
await self.async_update_downloaded_repositories()
self.set_stage(HacsStage.RUNNING)
self.async_dispatch(HacsDispatchEvent.RELOAD, {"force": True})
await self.async_handle_critical_repositories()
await self.async_prosess_queue()
self.async_dispatch(HacsDispatchEvent.STATUS, {})
async def async_download_file(self, url: str, *, headers: dict | None = None) -> bytes | None:
"""Download files, and return the content."""
if url is None:
return None
if "tags/" in url:
url = url.replace("tags/", "")
self.log.debug("Downloading %s", url)
timeouts = 0
while timeouts < 5:
try:
request = await self.session.get(
url=url,
timeout=ClientTimeout(total=60),
headers=headers,
)
# Make sure that we got a valid result
if request.status == 200:
return await request.read()
raise HacsException(
f"Got status code {request.status} when trying to download {url}"
)
except asyncio.TimeoutError:
self.log.warning(
"A timeout of 60! seconds was encountered while downloading %s, "
"using over 60 seconds to download a single file is not normal. "
"This is not a problem with HACS but how your host communicates with GitHub. "
"Retrying up to 5 times to mask/hide your host/network problems to "
"stop the flow of issues opened about it. "
"Tries left %s",
url,
(4 - timeouts),
)
timeouts += 1
await asyncio.sleep(1)
continue
except BaseException as exception: # lgtm [py/catch-base-exception] pylint: disable=broad-except
self.log.exception("Download failed - %s", exception)
return None
async def async_recreate_entities(self) -> None:
"""Recreate entities."""
if self.configuration == ConfigurationType.YAML or not self.configuration.experimental:
return
platforms = [Platform.SENSOR, Platform.UPDATE]
await self.hass.config_entries.async_unload_platforms(
entry=self.configuration.config_entry,
platforms=platforms,
)
await async_setup_entity_platforms(
self, self.hass, self.configuration.config_entry, platforms
)
@callback
def async_dispatch(self, signal: HacsDispatchEvent, data: dict | None = None) -> None:
"""Dispatch a signal with data."""
async_dispatcher_send(self.hass, signal, data)
def set_active_categories(self) -> None:
"""Set the active categories."""
self.common.categories = set()
for category in (HacsCategory.INTEGRATION, HacsCategory.PLUGIN):
self.enable_hacs_category(HacsCategory(category))
if HacsCategory.PYTHON_SCRIPT in self.hass.config.components:
self.enable_hacs_category(HacsCategory.PYTHON_SCRIPT)
if self.hass.services.has_service("frontend", "reload_themes"):
self.enable_hacs_category(HacsCategory.THEME)
if self.configuration.appdaemon:
self.enable_hacs_category(HacsCategory.APPDAEMON)
if self.configuration.netdaemon:
self.enable_hacs_category(HacsCategory.NETDAEMON)
async def async_get_all_category_repositories(self, _=None) -> None:
"""Get all category repositories."""
if self.system.disabled:
return
self.log.info("Loading known repositories")
await asyncio.gather(
*[
self.async_get_category_repositories(HacsCategory(category))
for category in self.common.categories or []
]
)
async def async_get_category_repositories(self, category: HacsCategory) -> None:
"""Get repositories from category."""
if self.system.disabled:
return
try:
repositories = await self.async_github_get_hacs_default_file(category)
except HacsException:
return
for repo in repositories:
if self.common.renamed_repositories.get(repo):
repo = self.common.renamed_repositories[repo]
if self.repositories.is_removed(repo):
continue
if repo in self.common.archived_repositories:
continue
repository = self.repositories.get_by_full_name(repo)
if repository is not None:
self.repositories.mark_default(repository)
if self.status.new and self.configuration.dev:
# Force update for new installations
self.queue.add(repository.common_update())
continue
self.queue.add(
self.async_register_repository(
repository_full_name=repo,
category=category,
default=True,
)
)
async def async_update_all_repositories(self, _=None) -> None:
"""Update all repositories."""
if self.system.disabled:
return
self.log.debug("Starting recurring background task for all repositories")
for repository in self.repositories.list_all:
if repository.data.category in self.common.categories:
self.queue.add(repository.common_update())
self.async_dispatch(HacsDispatchEvent.REPOSITORY, {"action": "reload"})
self.log.debug("Recurring background task for all repositories done")
async def async_check_rate_limit(self, _=None) -> None:
"""Check rate limit."""
if not self.system.disabled or self.system.disabled_reason != HacsDisabledReason.RATE_LIMIT:
return
self.log.debug("Checking if ratelimit has lifted")
can_update = await self.async_can_update()
self.log.debug("Ratelimit indicate we can update %s", can_update)
if can_update > 0:
self.enable_hacs()
await self.async_prosess_queue()
async def async_prosess_queue(self, _=None) -> None:
"""Process the queue."""
if self.system.disabled:
self.log.debug("HACS is disabled")
return
if not self.queue.has_pending_tasks:
self.log.debug("Nothing in the queue")
return
if self.queue.running:
self.log.debug("Queue is already running")
return
async def _handle_queue():
if not self.queue.has_pending_tasks:
await self.data.async_write()
return
can_update = await self.async_can_update()
self.log.debug(
"Can update %s repositories, " "items in queue %s",
can_update,
self.queue.pending_tasks,
)
if can_update != 0:
try:
await self.queue.execute(can_update)
except HacsExecutionStillInProgress:
return
await _handle_queue()
await _handle_queue()
async def async_handle_removed_repositories(self, _=None) -> None:
"""Handle removed repositories."""
if self.system.disabled:
return
need_to_save = False
self.log.info("Loading removed repositories")
try:
removed_repositories = await self.async_github_get_hacs_default_file(
HacsCategory.REMOVED
)
except HacsException:
return
for item in removed_repositories:
removed = self.repositories.removed_repository(item["repository"])
removed.update_data(item)
for removed in self.repositories.list_removed:
if (repository := self.repositories.get_by_full_name(removed.repository)) is None:
continue
if repository.data.full_name in self.common.ignored_repositories:
continue
if repository.data.installed and removed.removal_type != "critical":
self.log.warning(
"You have '%s' installed with HACS "
"this repository has been removed from HACS, please consider removing it. "
"Removal reason (%s)",
repository.data.full_name,
removed.reason,
)
else:
need_to_save = True
repository.remove()
if need_to_save:
await self.data.async_write()
async def async_update_downloaded_repositories(self, _=None) -> None:
"""Execute the task."""
if self.system.disabled:
return
self.log.info("Starting recurring background task for downloaded repositories")
for repository in self.repositories.list_downloaded:
if repository.data.category in self.common.categories:
self.queue.add(repository.update_repository(ignore_issues=True))
self.log.debug("Recurring background task for downloaded repositories done")
async def async_handle_critical_repositories(self, _=None) -> None:
"""Handle critical repositories."""
critical_queue = QueueManager(hass=self.hass)
instored = []
critical = []
was_installed = False
try:
critical = await self.async_github_get_hacs_default_file("critical")
except GitHubNotModifiedException:
return
except HacsException:
pass
if not critical:
self.log.debug("No critical repositories")
return
stored_critical = await async_load_from_store(self.hass, "critical")
for stored in stored_critical or []:
instored.append(stored["repository"])
stored_critical = []
for repository in critical:
removed_repo = self.repositories.removed_repository(repository["repository"])
removed_repo.removal_type = "critical"
repo = self.repositories.get_by_full_name(repository["repository"])
stored = {
"repository": repository["repository"],
"reason": repository["reason"],
"link": repository["link"],
"acknowledged": True,
}
if repository["repository"] not in instored:
if repo is not None and repo.data.installed:
self.log.critical(
"Removing repository %s, it is marked as critical",
repository["repository"],
)
was_installed = True
stored["acknowledged"] = False
# Remove from HACS
critical_queue.add(repo.uninstall())
repo.remove()
stored_critical.append(stored)
removed_repo.update_data(stored)
# Uninstall
await critical_queue.execute()
# Save to FS
await async_save_to_store(self.hass, "critical", stored_critical)
# Restart HASS
if was_installed:
self.log.critical("Restarting Home Assistant")
self.hass.async_create_task(self.hass.async_stop(100))
================================================
FILE: custom_components/hacs/config_flow.py
================================================
"""Adds config flow for HACS."""
from aiogithubapi import GitHubDeviceAPI, GitHubException
from aiogithubapi.common.const import OAUTH_USER_LOGIN
from awesomeversion import AwesomeVersion
from homeassistant import config_entries
from homeassistant.const import __version__ as HAVERSION
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.event import async_call_later
from homeassistant.loader import async_get_integration
import voluptuous as vol
from .base import HacsBase
from .const import CLIENT_ID, DOMAIN, MINIMUM_HA_VERSION
from .enums import ConfigurationType
from .utils.configuration_schema import RELEASE_LIMIT, hacs_config_option_schema
from .utils.logger import LOGGER
class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for HACS."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
"""Initialize."""
self._errors = {}
self.device = None
self.activation = None
self.log = LOGGER
self._progress_task = None
self._login_device = None
self._reauth = False
async def async_step_user(self, user_input):
"""Handle a flow initialized by the user."""
self._errors = {}
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
if self.hass.data.get(DOMAIN):
return self.async_abort(reason="single_instance_allowed")
if user_input:
if [x for x in user_input if not user_input[x]]:
self._errors["base"] = "acc"
return await self._show_config_form(user_input)
return await self.async_step_device(user_input)
## Initial form
return await self._show_config_form(user_input)
async def async_step_device(self, _user_input):
"""Handle device steps"""
async def _wait_for_activation(_=None):
if self._login_device is None or self._login_device.expires_in is None:
async_call_later(self.hass, 1, _wait_for_activation)
return
response = await self.device.activation(device_code=self._login_device.device_code)
self.activation = response.data
self.hass.async_create_task(
self.hass.config_entries.flow.async_configure(flow_id=self.flow_id)
)
if not self.activation:
integration = await async_get_integration(self.hass, DOMAIN)
if not self.device:
self.device = GitHubDeviceAPI(
client_id=CLIENT_ID,
session=aiohttp_client.async_get_clientsession(self.hass),
**{"client_name": f"HACS/{integration.version}"},
)
async_call_later(self.hass, 1, _wait_for_activation)
try:
response = await self.device.register()
self._login_device = response.data
return self.async_show_progress(
step_id="device",
progress_action="wait_for_device",
description_placeholders={
"url": OAUTH_USER_LOGIN,
"code": self._login_device.user_code,
},
)
except GitHubException as exception:
self.log.error(exception)
return self.async_abort(reason="github")
return self.async_show_progress_done(next_step_id="device_done")
async def _show_config_form(self, user_input):
"""Show the configuration form to edit location data."""
if not user_input:
user_input = {}
if AwesomeVersion(HAVERSION) < MINIMUM_HA_VERSION:
return self.async_abort(
reason="min_ha_version",
description_placeholders={"version": MINIMUM_HA_VERSION},
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required("acc_logs", default=user_input.get("acc_logs", False)): bool,
vol.Required("acc_addons", default=user_input.get("acc_addons", False)): bool,
vol.Required(
"acc_untested", default=user_input.get("acc_untested", False)
): bool,
vol.Required("acc_disable", default=user_input.get("acc_disable", False)): bool,
}
),
errors=self._errors,
)
async def async_step_device_done(self, _user_input):
"""Handle device steps"""
if self._reauth:
existing_entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
self.hass.config_entries.async_update_entry(
existing_entry, data={"token": self.activation.access_token}
)
await self.hass.config_entries.async_reload(existing_entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_create_entry(title="", data={"token": self.activation.access_token})
async def async_step_reauth(self, user_input=None):
"""Perform reauth upon an API authentication error."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(self, user_input=None):
"""Dialog that informs the user that reauth is required."""
if user_input is None:
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema({}),
)
self._reauth = True
return await self.async_step_device(None)
@staticmethod
@callback
def async_get_options_flow(config_entry):
return HacsOptionsFlowHandler(config_entry)
class HacsOptionsFlowHandler(config_entries.OptionsFlow):
"""HACS config flow options handler."""
def __init__(self, config_entry):
"""Initialize HACS options flow."""
self.config_entry = config_entry
async def async_step_init(self, _user_input=None):
"""Manage the options."""
return await self.async_step_user()
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
hacs: HacsBase = self.hass.data.get(DOMAIN)
if user_input is not None:
limit = int(user_input.get(RELEASE_LIMIT, 5))
if limit <= 0 or limit > 100:
return self.async_abort(reason="release_limit_value")
return self.async_create_entry(title="", data=user_input)
if hacs is None or hacs.configuration is None:
return self.async_abort(reason="not_setup")
if hacs.configuration.config_type == ConfigurationType.YAML:
schema = {vol.Optional("not_in_use", default=""): str}
else:
schema = hacs_config_option_schema(self.config_entry.options)
del schema["frontend_repo"]
del schema["frontend_repo_url"]
return self.async_show_form(step_id="user", data_schema=vol.Schema(schema))
================================================
FILE: custom_components/hacs/const.py
================================================
"""Constants for HACS"""
from typing import TypeVar
from aiogithubapi.common.const import ACCEPT_HEADERS
NAME_SHORT = "HACS"
DOMAIN = "hacs"
CLIENT_ID = "395a8e669c5de9f7c6e8"
MINIMUM_HA_VERSION = "2022.4.0"
TV = TypeVar("TV")
PACKAGE_NAME = "custom_components.hacs"
DEFAULT_CONCURRENT_TASKS = 15
DEFAULT_CONCURRENT_BACKOFF_TIME = 1
HACS_ACTION_GITHUB_API_HEADERS = {
"User-Agent": "HACS/action",
"Accept": ACCEPT_HEADERS["preview"],
}
VERSION_STORAGE = "6"
STORENAME = "hacs"
HACS_SYSTEM_ID = "0717a0cd-745c-48fd-9b16-c8534c9704f9-bc944b0f-fd42-4a58-a072-ade38d1444cd"
STARTUP = """
-------------------------------------------------------------------
HACS (Home Assistant Community Store)
Version: %s
This is a custom integration
If you have any issues with this you need to open an issue here:
https://github.com/hacs/integration/issues
-------------------------------------------------------------------
"""
LOCALE = [
"ALL",
"AF",
"AL",
"DZ",
"AS",
"AD",
"AO",
"AI",
"AQ",
"AG",
"AR",
"AM",
"AW",
"AU",
"AT",
"AZ",
"BS",
"BH",
"BD",
"BB",
"BY",
"BE",
"BZ",
"BJ",
"BM",
"BT",
"BO",
"BQ",
"BA",
"BW",
"BV",
"BR",
"IO",
"BN",
"BG",
"BF",
"BI",
"KH",
"CM",
"CA",
"CV",
"KY",
"CF",
"TD",
"CL",
"CN",
"CX",
"CC",
"CO",
"KM",
"CG",
"CD",
"CK",
"CR",
"HR",
"CU",
"CW",
"CY",
"CZ",
"CI",
"DK",
"DJ",
"DM",
"DO",
"EC",
"EG",
"SV",
"GQ",
"ER",
"EE",
"ET",
"FK",
"FO",
"FJ",
"FI",
"FR",
"GF",
"PF",
"TF",
"GA",
"GM",
"GE",
"DE",
"GH",
"GI",
"GR",
"GL",
"GD",
"GP",
"GU",
"GT",
"GG",
"GN",
"GW",
"GY",
"HT",
"HM",
"VA",
"HN",
"HK",
"HU",
"IS",
"IN",
"ID",
"IR",
"IQ",
"IE",
"IM",
"IL",
"IT",
"JM",
"JP",
"JE",
"JO",
"KZ",
"KE",
"KI",
"KP",
"KR",
"KW",
"KG",
"LA",
"LV",
"LB",
"LS",
"LR",
"LY",
"LI",
"LT",
"LU",
"MO",
"MK",
"MG",
"MW",
"MY",
"MV",
"ML",
"MT",
"MH",
"MQ",
"MR",
"MU",
"YT",
"MX",
"FM",
"MD",
"MC",
"MN",
"ME",
"MS",
"MA",
"MZ",
"MM",
"NA",
"NR",
"NP",
"NL",
"NC",
"NZ",
"NI",
"NE",
"NG",
"NU",
"NF",
"MP",
"NO",
"OM",
"PK",
"PW",
"PS",
"PA",
"PG",
"PY",
"PE",
"PH",
"PN",
"PL",
"PT",
"PR",
"QA",
"RO",
"RU",
"RW",
"RE",
"BL",
"SH",
"KN",
"LC",
"MF",
"PM",
"VC",
"WS",
"SM",
"ST",
"SA",
"SN",
"RS",
"SC",
"SL",
"SG",
"SX",
"SK",
"SI",
"SB",
"SO",
"ZA",
"GS",
"SS",
"ES",
"LK",
"SD",
"SR",
"SJ",
"SZ",
"SE",
"CH",
"SY",
"TW",
"TJ",
"TZ",
"TH",
"TL",
"TG",
"TK",
"TO",
"TT",
"TN",
"TR",
"TM",
"TC",
"TV",
"UG",
"UA",
"AE",
"GB",
"US",
"UM",
"UY",
"UZ",
"VU",
"VE",
"VN",
"VG",
"VI",
"WF",
"EH",
"YE",
"ZM",
"ZW",
]
================================================
FILE: custom_components/hacs/diagnostics.py
================================================
"""Diagnostics support for HACS."""
from __future__ import annotations
from typing import Any
from aiogithubapi import GitHubException
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .base import HacsBase
from .const import DOMAIN
from .utils.configuration_schema import TOKEN
async def async_get_config_entry_diagnostics(
hass: HomeAssistant,
entry: ConfigEntry,
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
hacs: HacsBase = hass.data[DOMAIN]
data = {
"entry": entry.as_dict(),
"hacs": {
"stage": hacs.stage,
"version": hacs.version,
"disabled_reason": hacs.system.disabled_reason,
"new": hacs.status.new,
"startup": hacs.status.startup,
"categories": hacs.common.categories,
"renamed_repositories": hacs.common.renamed_repositories,
"archived_repositories": hacs.common.archived_repositories,
"ignored_repositories": hacs.common.ignored_repositories,
"lovelace_mode": hacs.core.lovelace_mode,
"configuration": {},
},
"custom_repositories": [
repo.data.full_name
for repo in hacs.repositories.list_all
if not hacs.repositories.is_default(str(repo.data.id))
],
"repositories": [],
}
for key in (
"appdaemon",
"country",
"debug",
"dev",
"experimental",
"netdaemon",
"python_script",
"release_limit",
"theme",
):
data["hacs"]["configuration"][key] = getattr(hacs.configuration, key, None)
for repository in hacs.repositories.list_downloaded:
data["repositories"].append(
{
"data": repository.data.to_json(),
"integration_manifest": repository.integration_manifest,
"repository_manifest": repository.repository_manifest.to_dict(),
"ref": repository.ref,
"paths": {
"localpath": repository.localpath.replace(hacs.core.config_path, "/config"),
"local": repository.content.path.local.replace(
hacs.core.config_path, "/config"
),
"remote": repository.content.path.remote,
},
}
)
try:
rate_limit_response = await hacs.githubapi.rate_limit()
data["rate_limit"] = rate_limit_response.data.as_dict
except GitHubException as exception:
data["rate_limit"] = str(exception)
return async_redact_data(data, (TOKEN,))
================================================
FILE: custom_components/hacs/entity.py
================================================
"""HACS Base entities."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from .const import DOMAIN, HACS_SYSTEM_ID, NAME_SHORT
from .enums import HacsDispatchEvent, HacsGitHubRepo
if TYPE_CHECKING:
from .base import HacsBase
from .repositories.base import HacsRepository
def system_info(hacs: HacsBase) -> dict:
"""Return system info."""
return {
"identifiers": {(DOMAIN, HACS_SYSTEM_ID)},
"name": NAME_SHORT,
"manufacturer": "hacs.xyz",
"model": "",
"sw_version": str(hacs.version),
"configuration_url": "homeassistant://hacs",
"entry_type": DeviceEntryType.SERVICE,
}
class HacsBaseEntity(Entity):
"""Base HACS entity."""
repository: HacsRepository | None = None
_attr_should_poll = False
def __init__(self, hacs: HacsBase) -> None:
"""Initialize."""
self.hacs = hacs
async def async_added_to_hass(self) -> None:
"""Register for status events."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
HacsDispatchEvent.REPOSITORY,
self._update_and_write_state,
)
)
@callback
def _update(self) -> None:
"""Update the sensor."""
async def async_update(self) -> None:
"""Manual updates of the sensor."""
self._update()
@callback
def _update_and_write_state(self, _: Any) -> None:
"""Update the entity and write state."""
self._update()
self.async_write_ha_state()
class HacsSystemEntity(HacsBaseEntity):
"""Base system entity."""
_attr_icon = "hacs:hacs"
_attr_unique_id = HACS_SYSTEM_ID
@property
def device_info(self) -> dict[str, any]:
"""Return device information about HACS."""
return system_info(self.hacs)
class HacsRepositoryEntity(HacsBaseEntity):
"""Base repository entity."""
def __init__(
self,
hacs: HacsBase,
repository: HacsRepository,
) -> None:
"""Initialize."""
super().__init__(hacs=hacs)
self.repository = repository
self._attr_unique_id = str(repository.data.id)
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self.hacs.repositories.is_downloaded(repository_id=str(self.repository.data.id))
@property
def device_info(self) -> dict[str, any]:
"""Return device information about HACS."""
if self.repository.data.full_name == HacsGitHubRepo.INTEGRATION:
return system_info(self.hacs)
return {
"identifiers": {(DOMAIN, str(self.repository.data.id))},
"name": self.repository.display_name,
"model": self.repository.data.category,
"manufacturer": ", ".join(
author.replace("@", "") for author in self.repository.data.authors
),
"configuration_url": "homeassistant://hacs",
"entry_type": DeviceEntryType.SERVICE,
}
@callback
def _update_and_write_state(self, data: dict) -> None:
"""Update the entity and write state."""
if data.get("repository_id") == self.repository.data.id:
self._update()
self.async_write_ha_state()
================================================
FILE: custom_components/hacs/enums.py
================================================
"""Helper constants."""
# pylint: disable=missing-class-docstring
from enum import Enum
class HacsGitHubRepo(str, Enum):
"""HacsGitHubRepo."""
DEFAULT = "hacs/default"
INTEGRATION = "hacs/integration"
class HacsCategory(str, Enum):
APPDAEMON = "appdaemon"
INTEGRATION = "integration"
LOVELACE = "lovelace"
PLUGIN = "plugin" # Kept for legacy purposes
NETDAEMON = "netdaemon"
PYTHON_SCRIPT = "python_script"
THEME = "theme"
REMOVED = "removed"
def __str__(self):
return str(self.value)
class HacsDispatchEvent(str, Enum):
"""HacsDispatchEvent."""
CONFIG = "hacs_dispatch_config"
ERROR = "hacs_dispatch_error"
RELOAD = "hacs_dispatch_reload"
REPOSITORY = "hacs_dispatch_repository"
REPOSITORY_DOWNLOAD_PROGRESS = "hacs_dispatch_repository_download_progress"
STAGE = "hacs_dispatch_stage"
STARTUP = "hacs_dispatch_startup"
STATUS = "hacs_dispatch_status"
class RepositoryFile(str, Enum):
"""Repository file names."""
HACS_JSON = "hacs.json"
MAINIFEST_JSON = "manifest.json"
class ConfigurationType(str, Enum):
YAML = "yaml"
CONFIG_ENTRY = "config_entry"
class LovelaceMode(str, Enum):
"""Lovelace Modes."""
STORAGE = "storage"
AUTO = "auto"
AUTO_GEN = "auto-gen"
YAML = "yaml"
class HacsStage(str, Enum):
SETUP = "setup"
STARTUP = "startup"
WAITING = "waiting"
RUNNING = "running"
BACKGROUND = "background"
class HacsDisabledReason(str, Enum):
RATE_LIMIT = "rate_limit"
REMOVED = "removed"
INVALID_TOKEN = "invalid_token"
CONSTRAINS = "constrains"
LOAD_HACS = "load_hacs"
RESTORE = "restore"
================================================
FILE: custom_components/hacs/exceptions.py
================================================
"""Custom Exceptions for HACS."""
class HacsException(Exception):
"""Super basic."""
class HacsRepositoryArchivedException(HacsException):
"""For repositories that are archived."""
class HacsNotModifiedException(HacsException):
"""For responses that are not modified."""
class HacsExpectedException(HacsException):
"""For stuff that are expected."""
class HacsRepositoryExistException(HacsException):
"""For repositories that are already exist."""
class HacsExecutionStillInProgress(HacsException):
"""Exception to raise if execution is still in progress."""
class AddonRepositoryException(HacsException):
"""Exception to raise when user tries to add add-on repository."""
exception_message = (
"The repository does not seem to be a integration, "
"but an add-on repository. HACS does not manage add-ons."
)
def __init__(self) -> None:
super().__init__(self.exception_message)
class HomeAssistantCoreRepositoryException(HacsException):
"""Exception to raise when user tries to add the home-assistant/core repository."""
exception_message = (
"You can not add homeassistant/core, to use core integrations "
"check the Home Assistant documentation for how to add them."
)
def __init__(self) -> None:
super().__init__(self.exception_message)
================================================
FILE: custom_components/hacs/frontend.py
================================================
""""Starting setup task: Frontend"."""
from __future__ import annotations
from typing import TYPE_CHECKING
from aiohttp import web
from homeassistant.components.http import HomeAssistantView
from homeassistant.core import HomeAssistant, callback
from .const import DOMAIN
from .hacs_frontend import locate_dir
from .hacs_frontend.version import VERSION as FE_VERSION
URL_BASE = "/hacsfiles"
if TYPE_CHECKING:
from .base import HacsBase
@callback
def async_register_frontend(hass: HomeAssistant, hacs: HacsBase) -> None:
"""Register the frontend."""
# Register themes
hass.http.register_static_path(f"{URL_BASE}/themes", hass.config.path("themes"))
# Register frontend
if hacs.configuration.frontend_repo_url:
hacs.log.warning(
" Frontend development mode enabled. Do not run in production!"
)
hass.http.register_view(HacsFrontendDev())
else:
#
hass.http.register_static_path(f"{URL_BASE}/frontend", locate_dir(), cache_headers=False)
# Custom iconset
hass.http.register_static_path(
f"{URL_BASE}/iconset.js", str(hacs.integration_dir / "iconset.js")
)
if "frontend_extra_module_url" not in hass.data:
hass.data["frontend_extra_module_url"] = set()
hass.data["frontend_extra_module_url"].add(f"{URL_BASE}/iconset.js")
# Register www/community for all other files
use_cache = hacs.core.lovelace_mode == "storage"
hacs.log.info(
" %s mode, cache for /hacsfiles/: %s",
hacs.core.lovelace_mode,
use_cache,
)
hass.http.register_static_path(
URL_BASE,
hass.config.path("www/community"),
cache_headers=use_cache,
)
hacs.frontend_version = FE_VERSION
# Add to sidepanel if needed
if DOMAIN not in hass.data.get("frontend_panels", {}):
hass.components.frontend.async_register_built_in_panel(
component_name="custom",
sidebar_title=hacs.configuration.sidepanel_title,
sidebar_icon=hacs.configuration.sidepanel_icon,
frontend_url_path=DOMAIN,
config={
"_panel_custom": {
"name": "hacs-frontend",
"embed_iframe": True,
"trust_external": False,
"js_url": f"/hacsfiles/frontend/entrypoint.js?hacstag={FE_VERSION}",
}
},
require_admin=True,
)
class HacsFrontendDev(HomeAssistantView):
"""Dev View Class for HACS."""
requires_auth = False
name = "hacs_files:frontend"
url = r"/hacsfiles/frontend/{requested_file:.+}"
async def get(self, request, requested_file): # pylint: disable=unused-argument
"""Handle HACS Web requests."""
hacs: HacsBase = request.app["hass"].data.get(DOMAIN)
requested = requested_file.split("/")[-1]
request = await hacs.session.get(f"{hacs.configuration.frontend_repo_url}/{requested}")
if request.status == 200:
result = await request.read()
response = web.Response(body=result)
response.headers["Content-Type"] = "application/javascript"
return response
================================================
FILE: custom_components/hacs/hacs_frontend/__init__.py
================================================
"""HACS Frontend"""
from .version import VERSION
def locate_dir():
return __path__[0]
================================================
FILE: custom_components/hacs/hacs_frontend/c.004a7b01.js
================================================
var t=function(){if("undefined"!=typeof Map)return Map;function t(t,e){var n=-1;return t.some((function(t,r){return t[0]===e&&(n=r,!0)})),n}return function(){function e(){this.__entries__=[]}return Object.defineProperty(e.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),e.prototype.get=function(e){var n=t(this.__entries__,e),r=this.__entries__[n];return r&&r[1]},e.prototype.set=function(e,n){var r=t(this.__entries__,e);~r?this.__entries__[r][1]=n:this.__entries__.push([e,n])},e.prototype.delete=function(e){var n=this.__entries__,r=t(n,e);~r&&n.splice(r,1)},e.prototype.has=function(e){return!!~t(this.__entries__,e)},e.prototype.clear=function(){this.__entries__.splice(0)},e.prototype.forEach=function(t,e){void 0===e&&(e=null);for(var n=0,r=this.__entries__;n0},t.prototype.connect_=function(){e&&!this.connected_&&(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),o?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},t.prototype.disconnect_=function(){e&&this.connected_&&(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},t.prototype.onTransitionEnd_=function(t){var e=t.propertyName,n=void 0===e?"":e;i.some((function(t){return!!~n.indexOf(t)}))&&this.refresh()},t.getInstance=function(){return this.instance_||(this.instance_=new t),this.instance_},t.instance_=null,t}(),a=function(t,e){for(var n=0,r=Object.keys(e);n0},e}(),y="undefined"!=typeof WeakMap?new WeakMap:new t,g=function t(e){if(!(this instanceof t))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var n=s.getInstance(),r=new m(e,n,this);y.set(this,r)};["observe","unobserve","disconnect"].forEach((function(t){g.prototype[t]=function(){var e;return(e=y.get(this))[t].apply(e,arguments)}}));var w=void 0!==n.ResizeObserver?n.ResizeObserver:g;export{w as default};
================================================
FILE: custom_components/hacs/hacs_frontend/c.01f18260.js
================================================
import{u as e,v as t,G as i,M as c,_ as o,i as r,e as n,t as a,B as d,$ as s,o as p,I as l,y as m,p as h,q as u,r as f,n as b,a as g,h as k,J as x,K as _,g as y,w as v,R as T,j as w,A as E}from"./main-7bc9a818.js";import{c as O,o as I}from"./c.5d9598b2.js";import{o as C}from"./c.8e28b461.js";var A,R,S={ANCHOR:"mdc-menu-surface--anchor",ANIMATING_CLOSED:"mdc-menu-surface--animating-closed",ANIMATING_OPEN:"mdc-menu-surface--animating-open",FIXED:"mdc-menu-surface--fixed",IS_OPEN_BELOW:"mdc-menu-surface--is-open-below",OPEN:"mdc-menu-surface--open",ROOT:"mdc-menu-surface"},F={CLOSED_EVENT:"MDCMenuSurface:closed",CLOSING_EVENT:"MDCMenuSurface:closing",OPENED_EVENT:"MDCMenuSurface:opened",FOCUSABLE_ELEMENTS:["button:not(:disabled)",'[href]:not([aria-disabled="true"])',"input:not(:disabled)","select:not(:disabled)","textarea:not(:disabled)",'[tabindex]:not([tabindex="-1"]):not([aria-disabled="true"])'].join(", ")},B={TRANSITION_OPEN_DURATION:120,TRANSITION_CLOSE_DURATION:75,MARGIN_TO_EDGE:32,ANCHOR_TO_MENU_SURFACE_WIDTH_RATIO:.67,TOUCH_EVENT_WAIT_MS:30};!function(e){e[e.BOTTOM=1]="BOTTOM",e[e.CENTER=2]="CENTER",e[e.RIGHT=4]="RIGHT",e[e.FLIP_RTL=8]="FLIP_RTL"}(A||(A={})),function(e){e[e.TOP_LEFT=0]="TOP_LEFT",e[e.TOP_RIGHT=4]="TOP_RIGHT",e[e.BOTTOM_LEFT=1]="BOTTOM_LEFT",e[e.BOTTOM_RIGHT=5]="BOTTOM_RIGHT",e[e.TOP_START=8]="TOP_START",e[e.TOP_END=12]="TOP_END",e[e.BOTTOM_START=9]="BOTTOM_START",e[e.BOTTOM_END=13]="BOTTOM_END"}(R||(R={}));var M=function(c){function o(e){var i=c.call(this,t(t({},o.defaultAdapter),e))||this;return i.isSurfaceOpen=!1,i.isQuickOpen=!1,i.isHoistedElement=!1,i.isFixedPosition=!1,i.isHorizontallyCenteredOnViewport=!1,i.maxHeight=0,i.openBottomBias=0,i.openAnimationEndTimerId=0,i.closeAnimationEndTimerId=0,i.animationRequestId=0,i.anchorCorner=R.TOP_START,i.originCorner=R.TOP_START,i.anchorMargin={top:0,right:0,bottom:0,left:0},i.position={x:0,y:0},i}return e(o,c),Object.defineProperty(o,"cssClasses",{get:function(){return S},enumerable:!1,configurable:!0}),Object.defineProperty(o,"strings",{get:function(){return F},enumerable:!1,configurable:!0}),Object.defineProperty(o,"numbers",{get:function(){return B},enumerable:!1,configurable:!0}),Object.defineProperty(o,"Corner",{get:function(){return R},enumerable:!1,configurable:!0}),Object.defineProperty(o,"defaultAdapter",{get:function(){return{addClass:function(){},removeClass:function(){},hasClass:function(){return!1},hasAnchor:function(){return!1},isElementInContainer:function(){return!1},isFocused:function(){return!1},isRtl:function(){return!1},getInnerDimensions:function(){return{height:0,width:0}},getAnchorDimensions:function(){return null},getWindowDimensions:function(){return{height:0,width:0}},getBodyDimensions:function(){return{height:0,width:0}},getWindowScroll:function(){return{x:0,y:0}},setPosition:function(){},setMaxHeight:function(){},setTransformOrigin:function(){},saveFocus:function(){},restoreFocus:function(){},notifyClose:function(){},notifyOpen:function(){},notifyClosing:function(){}}},enumerable:!1,configurable:!0}),o.prototype.init=function(){var e=o.cssClasses,t=e.ROOT,i=e.OPEN;if(!this.adapter.hasClass(t))throw new Error(t+" class required in root element.");this.adapter.hasClass(i)&&(this.isSurfaceOpen=!0)},o.prototype.destroy=function(){clearTimeout(this.openAnimationEndTimerId),clearTimeout(this.closeAnimationEndTimerId),cancelAnimationFrame(this.animationRequestId)},o.prototype.setAnchorCorner=function(e){this.anchorCorner=e},o.prototype.flipCornerHorizontally=function(){this.originCorner=this.originCorner^A.RIGHT},o.prototype.setAnchorMargin=function(e){this.anchorMargin.top=e.top||0,this.anchorMargin.right=e.right||0,this.anchorMargin.bottom=e.bottom||0,this.anchorMargin.left=e.left||0},o.prototype.setIsHoisted=function(e){this.isHoistedElement=e},o.prototype.setFixedPosition=function(e){this.isFixedPosition=e},o.prototype.isFixed=function(){return this.isFixedPosition},o.prototype.setAbsolutePosition=function(e,t){this.position.x=this.isFinite(e)?e:0,this.position.y=this.isFinite(t)?t:0},o.prototype.setIsHorizontallyCenteredOnViewport=function(e){this.isHorizontallyCenteredOnViewport=e},o.prototype.setQuickOpen=function(e){this.isQuickOpen=e},o.prototype.setMaxHeight=function(e){this.maxHeight=e},o.prototype.setOpenBottomBias=function(e){this.openBottomBias=e},o.prototype.isOpen=function(){return this.isSurfaceOpen},o.prototype.open=function(){var e=this;this.isSurfaceOpen||(this.adapter.saveFocus(),this.isQuickOpen?(this.isSurfaceOpen=!0,this.adapter.addClass(o.cssClasses.OPEN),this.dimensions=this.adapter.getInnerDimensions(),this.autoposition(),this.adapter.notifyOpen()):(this.adapter.addClass(o.cssClasses.ANIMATING_OPEN),this.animationRequestId=requestAnimationFrame((function(){e.dimensions=e.adapter.getInnerDimensions(),e.autoposition(),e.adapter.addClass(o.cssClasses.OPEN),e.openAnimationEndTimerId=setTimeout((function(){e.openAnimationEndTimerId=0,e.adapter.removeClass(o.cssClasses.ANIMATING_OPEN),e.adapter.notifyOpen()}),B.TRANSITION_OPEN_DURATION)})),this.isSurfaceOpen=!0))},o.prototype.close=function(e){var t=this;if(void 0===e&&(e=!1),this.isSurfaceOpen){if(this.adapter.notifyClosing(),this.isQuickOpen)return this.isSurfaceOpen=!1,e||this.maybeRestoreFocus(),this.adapter.removeClass(o.cssClasses.OPEN),this.adapter.removeClass(o.cssClasses.IS_OPEN_BELOW),void this.adapter.notifyClose();this.adapter.addClass(o.cssClasses.ANIMATING_CLOSED),requestAnimationFrame((function(){t.adapter.removeClass(o.cssClasses.OPEN),t.adapter.removeClass(o.cssClasses.IS_OPEN_BELOW),t.closeAnimationEndTimerId=setTimeout((function(){t.closeAnimationEndTimerId=0,t.adapter.removeClass(o.cssClasses.ANIMATING_CLOSED),t.adapter.notifyClose()}),B.TRANSITION_CLOSE_DURATION)})),this.isSurfaceOpen=!1,e||this.maybeRestoreFocus()}},o.prototype.handleBodyClick=function(e){var t=e.target;this.adapter.isElementInContainer(t)||this.close()},o.prototype.handleKeydown=function(e){var t=e.keyCode;("Escape"===e.key||27===t)&&this.close()},o.prototype.autoposition=function(){var e;this.measurements=this.getAutoLayoutmeasurements();var t=this.getoriginCorner(),i=this.getMenuSurfaceMaxHeight(t),c=this.hasBit(t,A.BOTTOM)?"bottom":"top",r=this.hasBit(t,A.RIGHT)?"right":"left",n=this.getHorizontalOriginOffset(t),a=this.getVerticalOriginOffset(t),d=this.measurements,s=d.anchorSize,p=d.surfaceSize,l=((e={})[r]=n,e[c]=a,e);s.width/p.width>B.ANCHOR_TO_MENU_SURFACE_WIDTH_RATIO&&(r="center"),(this.isHoistedElement||this.isFixedPosition)&&this.adjustPositionForHoistedElement(l),this.adapter.setTransformOrigin(r+" "+c),this.adapter.setPosition(l),this.adapter.setMaxHeight(i?i+"px":""),this.hasBit(t,A.BOTTOM)||this.adapter.addClass(o.cssClasses.IS_OPEN_BELOW)},o.prototype.getAutoLayoutmeasurements=function(){var e=this.adapter.getAnchorDimensions(),t=this.adapter.getBodyDimensions(),i=this.adapter.getWindowDimensions(),c=this.adapter.getWindowScroll();return e||(e={top:this.position.y,right:this.position.x,bottom:this.position.y,left:this.position.x,width:0,height:0}),{anchorSize:e,bodySize:t,surfaceSize:this.dimensions,viewportDistance:{top:e.top,right:i.width-e.right,bottom:i.height-e.bottom,left:e.left},viewportSize:i,windowScroll:c}},o.prototype.getoriginCorner=function(){var e,t,i=this.originCorner,c=this.measurements,r=c.viewportDistance,n=c.anchorSize,a=c.surfaceSize,d=o.numbers.MARGIN_TO_EDGE;this.hasBit(this.anchorCorner,A.BOTTOM)?(e=r.top-d+this.anchorMargin.bottom,t=r.bottom-d-this.anchorMargin.bottom):(e=r.top-d+this.anchorMargin.top,t=r.bottom-d+n.height-this.anchorMargin.top),!(t-a.height>0)&&e>t+this.openBottomBias&&(i=this.setBit(i,A.BOTTOM));var s,p,l=this.adapter.isRtl(),m=this.hasBit(this.anchorCorner,A.FLIP_RTL),h=this.hasBit(this.anchorCorner,A.RIGHT)||this.hasBit(i,A.RIGHT),u=!1;(u=l&&m?!h:h)?(s=r.left+n.width+this.anchorMargin.right,p=r.right-this.anchorMargin.right):(s=r.left+this.anchorMargin.left,p=r.right+n.width-this.anchorMargin.left);var f=s-a.width>0,b=p-a.width>0,g=this.hasBit(i,A.FLIP_RTL)&&this.hasBit(i,A.RIGHT);return b&&g&&l||!f&&g?i=this.unsetBit(i,A.RIGHT):(f&&u&&l||f&&!u&&h||!b&&s>=p)&&(i=this.setBit(i,A.RIGHT)),i},o.prototype.getMenuSurfaceMaxHeight=function(e){if(this.maxHeight>0)return this.maxHeight;var t=this.measurements.viewportDistance,i=0,c=this.hasBit(e,A.BOTTOM),r=this.hasBit(this.anchorCorner,A.BOTTOM),n=o.numbers.MARGIN_TO_EDGE;return c?(i=t.top+this.anchorMargin.top-n,r||(i+=this.measurements.anchorSize.height)):(i=t.bottom-this.anchorMargin.bottom+this.measurements.anchorSize.height-n,r&&(i-=this.measurements.anchorSize.height)),i},o.prototype.getHorizontalOriginOffset=function(e){var t=this.measurements.anchorSize,i=this.hasBit(e,A.RIGHT),c=this.hasBit(this.anchorCorner,A.RIGHT);if(i){var o=c?t.width-this.anchorMargin.left:this.anchorMargin.right;return this.isHoistedElement||this.isFixedPosition?o-(this.measurements.viewportSize.width-this.measurements.bodySize.width):o}return c?t.width-this.anchorMargin.right:this.anchorMargin.left},o.prototype.getVerticalOriginOffset=function(e){var t=this.measurements.anchorSize,i=this.hasBit(e,A.BOTTOM),c=this.hasBit(this.anchorCorner,A.BOTTOM);return i?c?t.height-this.anchorMargin.top:-this.anchorMargin.bottom:c?t.height+this.anchorMargin.bottom:this.anchorMargin.top},o.prototype.adjustPositionForHoistedElement=function(e){var t,c,o=this.measurements,r=o.windowScroll,n=o.viewportDistance,a=o.surfaceSize,d=o.viewportSize,s=Object.keys(e);try{for(var p=i(s),l=p.next();!l.done;l=p.next()){var m=l.value,h=e[m]||0;!this.isHorizontallyCenteredOnViewport||"left"!==m&&"right"!==m?(h+=n[m],this.isFixedPosition||("top"===m?h+=r.y:"bottom"===m?h-=r.y:"left"===m?h+=r.x:h-=r.x),e[m]=h):e[m]=(d.width-a.width)/2}}catch(e){t={error:e}}finally{try{l&&!l.done&&(c=p.return)&&c.call(p)}finally{if(t)throw t.error}}},o.prototype.maybeRestoreFocus=function(){var e=this,t=this.adapter.isFocused(),i=document.activeElement&&this.adapter.isElementInContainer(document.activeElement);(t||i)&&setTimeout((function(){e.adapter.restoreFocus()}),B.TOUCH_EVENT_WAIT_MS)},o.prototype.hasBit=function(e,t){return Boolean(e&t)},o.prototype.setBit=function(e,t){return e|t},o.prototype.unsetBit=function(e,t){return e^t},o.prototype.isFinite=function(e){return"number"==typeof e&&isFinite(e)},o}(c),z=M;const L={TOP_LEFT:R.TOP_LEFT,TOP_RIGHT:R.TOP_RIGHT,BOTTOM_LEFT:R.BOTTOM_LEFT,BOTTOM_RIGHT:R.BOTTOM_RIGHT,TOP_START:R.TOP_START,TOP_END:R.TOP_END,BOTTOM_START:R.BOTTOM_START,BOTTOM_END:R.BOTTOM_END};class N extends d{constructor(){super(...arguments),this.mdcFoundationClass=z,this.absolute=!1,this.fullwidth=!1,this.fixed=!1,this.x=null,this.y=null,this.quick=!1,this.open=!1,this.stayOpenOnBodyClick=!1,this.bitwiseCorner=R.TOP_START,this.previousMenuCorner=null,this.menuCorner="START",this.corner="TOP_START",this.styleTop="",this.styleLeft="",this.styleRight="",this.styleBottom="",this.styleMaxHeight="",this.styleTransformOrigin="",this.anchor=null,this.previouslyFocused=null,this.previousAnchor=null,this.onBodyClickBound=()=>{}}render(){const e={"mdc-menu-surface--fixed":this.fixed,"mdc-menu-surface--fullwidth":this.fullwidth},t={top:this.styleTop,left:this.styleLeft,right:this.styleRight,bottom:this.styleBottom,"max-height":this.styleMaxHeight,"transform-origin":this.styleTransformOrigin};return s`
`}createAdapter(){return Object.assign(Object.assign({},m(this.mdcRoot)),{hasAnchor:()=>!!this.anchor,notifyClose:()=>{const e=new CustomEvent("closed",{bubbles:!0,composed:!0});this.open=!1,this.mdcRoot.dispatchEvent(e)},notifyClosing:()=>{const e=new CustomEvent("closing",{bubbles:!0,composed:!0});this.mdcRoot.dispatchEvent(e)},notifyOpen:()=>{const e=new CustomEvent("opened",{bubbles:!0,composed:!0});this.open=!0,this.mdcRoot.dispatchEvent(e)},isElementInContainer:()=>!1,isRtl:()=>!!this.mdcRoot&&"rtl"===getComputedStyle(this.mdcRoot).direction,setTransformOrigin:e=>{this.mdcRoot&&(this.styleTransformOrigin=e)},isFocused:()=>h(this),saveFocus:()=>{const e=u(),t=e.length;t||(this.previouslyFocused=null),this.previouslyFocused=e[t-1]},restoreFocus:()=>{this.previouslyFocused&&"focus"in this.previouslyFocused&&this.previouslyFocused.focus()},getInnerDimensions:()=>{const e=this.mdcRoot;return e?{width:e.offsetWidth,height:e.offsetHeight}:{width:0,height:0}},getAnchorDimensions:()=>{const e=this.anchor;return e?e.getBoundingClientRect():null},getBodyDimensions:()=>({width:document.body.clientWidth,height:document.body.clientHeight}),getWindowDimensions:()=>({width:window.innerWidth,height:window.innerHeight}),getWindowScroll:()=>({x:window.pageXOffset,y:window.pageYOffset}),setPosition:e=>{this.mdcRoot&&(this.styleLeft="left"in e?`${e.left}px`:"",this.styleRight="right"in e?`${e.right}px`:"",this.styleTop="top"in e?`${e.top}px`:"",this.styleBottom="bottom"in e?`${e.bottom}px`:"")},setMaxHeight:async e=>{this.mdcRoot&&(this.styleMaxHeight=e,await this.updateComplete,this.styleMaxHeight=`var(--mdc-menu-max-height, ${e})`)}})}onKeydown(e){this.mdcFoundation&&this.mdcFoundation.handleKeydown(e)}onBodyClick(e){if(this.stayOpenOnBodyClick)return;-1===e.composedPath().indexOf(this)&&this.close()}registerBodyClick(){this.onBodyClickBound=this.onBodyClick.bind(this),document.body.addEventListener("click",this.onBodyClickBound,{passive:!0,capture:!0})}deregisterBodyClick(){document.body.removeEventListener("click",this.onBodyClickBound,{capture:!0})}close(){this.open=!1}show(){this.open=!0}}o([r(".mdc-menu-surface")],N.prototype,"mdcRoot",void 0),o([r("slot")],N.prototype,"slotElement",void 0),o([n({type:Boolean}),C((function(e){this.mdcFoundation&&!this.fixed&&this.mdcFoundation.setIsHoisted(e)}))],N.prototype,"absolute",void 0),o([n({type:Boolean})],N.prototype,"fullwidth",void 0),o([n({type:Boolean}),C((function(e){this.mdcFoundation&&!this.absolute&&this.mdcFoundation.setFixedPosition(e)}))],N.prototype,"fixed",void 0),o([n({type:Number}),C((function(e){this.mdcFoundation&&null!==this.y&&null!==e&&(this.mdcFoundation.setAbsolutePosition(e,this.y),this.mdcFoundation.setAnchorMargin({left:e,top:this.y,right:-e,bottom:this.y}))}))],N.prototype,"x",void 0),o([n({type:Number}),C((function(e){this.mdcFoundation&&null!==this.x&&null!==e&&(this.mdcFoundation.setAbsolutePosition(this.x,e),this.mdcFoundation.setAnchorMargin({left:this.x,top:e,right:-this.x,bottom:e}))}))],N.prototype,"y",void 0),o([n({type:Boolean}),C((function(e){this.mdcFoundation&&this.mdcFoundation.setQuickOpen(e)}))],N.prototype,"quick",void 0),o([n({type:Boolean,reflect:!0}),C((function(e,t){this.mdcFoundation&&(e?this.mdcFoundation.open():void 0!==t&&this.mdcFoundation.close())}))],N.prototype,"open",void 0),o([n({type:Boolean})],N.prototype,"stayOpenOnBodyClick",void 0),o([a(),C((function(e){this.mdcFoundation&&this.mdcFoundation.setAnchorCorner(e)}))],N.prototype,"bitwiseCorner",void 0),o([n({type:String}),C((function(e){if(this.mdcFoundation){const t="START"===e||"END"===e,i=null===this.previousMenuCorner,c=!i&&e!==this.previousMenuCorner,o=i&&"END"===e;t&&(c||o)&&(this.bitwiseCorner=this.bitwiseCorner^A.RIGHT,this.mdcFoundation.flipCornerHorizontally(),this.previousMenuCorner=e)}}))],N.prototype,"menuCorner",void 0),o([n({type:String}),C((function(e){if(this.mdcFoundation&&e){let t=L[e];"END"===this.menuCorner&&(t^=A.RIGHT),this.bitwiseCorner=t}}))],N.prototype,"corner",void 0),o([a()],N.prototype,"styleTop",void 0),o([a()],N.prototype,"styleLeft",void 0),o([a()],N.prototype,"styleRight",void 0),o([a()],N.prototype,"styleBottom",void 0),o([a()],N.prototype,"styleMaxHeight",void 0),o([a()],N.prototype,"styleTransformOrigin",void 0);const D=f`.mdc-menu-surface{display:none;position:absolute;box-sizing:border-box;max-width:calc(100vw - 32px);max-width:var(--mdc-menu-max-width, calc(100vw - 32px));max-height:calc(100vh - 32px);max-height:var(--mdc-menu-max-height, calc(100vh - 32px));margin:0;padding:0;transform:scale(1);transform-origin:top left;opacity:0;overflow:auto;will-change:transform,opacity;z-index:8;transition:opacity .03s linear,transform .12s cubic-bezier(0, 0, 0.2, 1),height 250ms cubic-bezier(0, 0, 0.2, 1);box-shadow:0px 5px 5px -3px rgba(0, 0, 0, 0.2),0px 8px 10px 1px rgba(0, 0, 0, 0.14),0px 3px 14px 2px rgba(0,0,0,.12);background-color:#fff;background-color:var(--mdc-theme-surface, #fff);color:#000;color:var(--mdc-theme-on-surface, #000);border-radius:4px;border-radius:var(--mdc-shape-medium, 4px);transform-origin-left:top left;transform-origin-right:top right}.mdc-menu-surface:focus{outline:none}.mdc-menu-surface--animating-open{display:inline-block;transform:scale(0.8);opacity:0}.mdc-menu-surface--open{display:inline-block;transform:scale(1);opacity:1}.mdc-menu-surface--animating-closed{display:inline-block;opacity:0;transition:opacity .075s linear}[dir=rtl] .mdc-menu-surface,.mdc-menu-surface[dir=rtl]{transform-origin-left:top right;transform-origin-right:top left}.mdc-menu-surface--anchor{position:relative;overflow:visible}.mdc-menu-surface--fixed{position:fixed}.mdc-menu-surface--fullwidth{width:100%}:host(:not([open])){display:none}.mdc-menu-surface{z-index:8;z-index:var(--mdc-menu-z-index, 8);min-width:112px;min-width:var(--mdc-menu-min-width, 112px)}`;let H=class extends N{};H.styles=[D],H=o([b("mwc-menu-surface")],H);var P,$={MENU_SELECTED_LIST_ITEM:"mdc-menu-item--selected",MENU_SELECTION_GROUP:"mdc-menu__selection-group",ROOT:"mdc-menu"},G={ARIA_CHECKED_ATTR:"aria-checked",ARIA_DISABLED_ATTR:"aria-disabled",CHECKBOX_SELECTOR:'input[type="checkbox"]',LIST_SELECTOR:".mdc-list,.mdc-deprecated-list",SELECTED_EVENT:"MDCMenu:selected",SKIP_RESTORE_FOCUS:"data-menu-item-skip-restore-focus"},U={FOCUS_ROOT_INDEX:-1};!function(e){e[e.NONE=0]="NONE",e[e.LIST_ROOT=1]="LIST_ROOT",e[e.FIRST_ITEM=2]="FIRST_ITEM",e[e.LAST_ITEM=3]="LAST_ITEM"}(P||(P={}));var j=function(i){function c(e){var o=i.call(this,t(t({},c.defaultAdapter),e))||this;return o.closeAnimationEndTimerId=0,o.defaultFocusState=P.LIST_ROOT,o.selectedIndex=-1,o}return e(c,i),Object.defineProperty(c,"cssClasses",{get:function(){return $},enumerable:!1,configurable:!0}),Object.defineProperty(c,"strings",{get:function(){return G},enumerable:!1,configurable:!0}),Object.defineProperty(c,"numbers",{get:function(){return U},enumerable:!1,configurable:!0}),Object.defineProperty(c,"defaultAdapter",{get:function(){return{addClassToElementAtIndex:function(){},removeClassFromElementAtIndex:function(){},addAttributeToElementAtIndex:function(){},removeAttributeFromElementAtIndex:function(){},getAttributeFromElementAtIndex:function(){return null},elementContainsClass:function(){return!1},closeSurface:function(){},getElementIndex:function(){return-1},notifySelected:function(){},getMenuItemCount:function(){return 0},focusItemAtIndex:function(){},focusListRoot:function(){},getSelectedSiblingOfItemAtIndex:function(){return-1},isSelectableItemAtIndex:function(){return!1}}},enumerable:!1,configurable:!0}),c.prototype.destroy=function(){this.closeAnimationEndTimerId&&clearTimeout(this.closeAnimationEndTimerId),this.adapter.closeSurface()},c.prototype.handleKeydown=function(e){var t=e.key,i=e.keyCode;("Tab"===t||9===i)&&this.adapter.closeSurface(!0)},c.prototype.handleItemAction=function(e){var t=this,i=this.adapter.getElementIndex(e);if(!(i<0)){this.adapter.notifySelected({index:i});var c="true"===this.adapter.getAttributeFromElementAtIndex(i,G.SKIP_RESTORE_FOCUS);this.adapter.closeSurface(c),this.closeAnimationEndTimerId=setTimeout((function(){var i=t.adapter.getElementIndex(e);i>=0&&t.adapter.isSelectableItemAtIndex(i)&&t.setSelectedIndex(i)}),M.numbers.TRANSITION_CLOSE_DURATION)}},c.prototype.handleMenuSurfaceOpened=function(){switch(this.defaultFocusState){case P.FIRST_ITEM:this.adapter.focusItemAtIndex(0);break;case P.LAST_ITEM:this.adapter.focusItemAtIndex(this.adapter.getMenuItemCount()-1);break;case P.NONE:break;default:this.adapter.focusListRoot()}},c.prototype.setDefaultFocusState=function(e){this.defaultFocusState=e},c.prototype.getSelectedIndex=function(){return this.selectedIndex},c.prototype.setSelectedIndex=function(e){if(this.validatedIndex(e),!this.adapter.isSelectableItemAtIndex(e))throw new Error("MDCMenuFoundation: No selection group at specified index.");var t=this.adapter.getSelectedSiblingOfItemAtIndex(e);t>=0&&(this.adapter.removeAttributeFromElementAtIndex(t,G.ARIA_CHECKED_ATTR),this.adapter.removeClassFromElementAtIndex(t,$.MENU_SELECTED_LIST_ITEM)),this.adapter.addClassToElementAtIndex(e,$.MENU_SELECTED_LIST_ITEM),this.adapter.addAttributeToElementAtIndex(e,G.ARIA_CHECKED_ATTR,"true"),this.selectedIndex=e},c.prototype.setEnabled=function(e,t){this.validatedIndex(e),t?(this.adapter.removeClassFromElementAtIndex(e,O.LIST_ITEM_DISABLED_CLASS),this.adapter.addAttributeToElementAtIndex(e,G.ARIA_DISABLED_ATTR,"false")):(this.adapter.addClassToElementAtIndex(e,O.LIST_ITEM_DISABLED_CLASS),this.adapter.addAttributeToElementAtIndex(e,G.ARIA_DISABLED_ATTR,"true"))},c.prototype.validatedIndex=function(e){var t=this.adapter.getMenuItemCount();if(!(e>=0&&e