diff --git a/dist/mushroom-strategy.js b/dist/mushroom-strategy.js deleted file mode 100644 index 6f98476..0000000 --- a/dist/mushroom-strategy.js +++ /dev/null @@ -1 +0,0 @@ -(()=>{var e,t,r={744:e=>{"use strict";var t=function(e){return function(e){return!!e&&"object"==typeof e}(e)&&!function(e){var t=Object.prototype.toString.call(e);return"[object RegExp]"===t||"[object Date]"===t||function(e){return e.$$typeof===r}(e)}(e)},r="function"==typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function i(e,t){return!1!==t.clone&&t.isMergeableObject(e)?n((r=e,Array.isArray(r)?[]:{}),e,t):e;var r}function a(e,t,r){return e.concat(t).map((function(e){return i(e,r)}))}function o(e){return Object.keys(e).concat(function(e){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(e).filter((function(t){return Object.propertyIsEnumerable.call(e,t)})):[]}(e))}function s(e,t){try{return t in e}catch(e){return!1}}function n(e,r,c){(c=c||{}).arrayMerge=c.arrayMerge||a,c.isMergeableObject=c.isMergeableObject||t,c.cloneUnlessOtherwiseSpecified=i;var l=Array.isArray(r);return l===Array.isArray(e)?l?c.arrayMerge(e,r,c):function(e,t,r){var a={};return r.isMergeableObject(e)&&o(e).forEach((function(t){a[t]=i(e[t],r)})),o(t).forEach((function(o){(function(e,t){return s(e,t)&&!(Object.hasOwnProperty.call(e,t)&&Object.propertyIsEnumerable.call(e,t))})(e,o)||(s(e,o)&&r.isMergeableObject(t[o])?a[o]=function(e,t){if(!t.customMerge)return n;var r=t.customMerge(e);return"function"==typeof r?r:n}(o,r)(e[o],t[o],r):a[o]=i(t[o],r))})),a}(e,r,c):i(r,c)}n.all=function(e,t){if(!Array.isArray(e))throw new Error("first argument should be an array");return e.reduce((function(e,r){return n(e,r,t)}),{})};var c=n;e.exports=c},245:(e,t,r)=>{"use strict";r.d(t,{H:()=>j});var i=r(744),a=r.n(i);const o=JSON.parse('{"camera":{"all_cameras":"All Cameras","cameras":"Cameras"},"climate":{"all_climates":"All Climates","climates":"Climates"},"cover":{"all_covers":"All Covers","covers":"Covers"},"fan":{"all_fans":"All Fans","fans":"Fans"},"generic":{"all":"All","areas":"Areas","busy":"Busy","good_afternoon":"Good afternoon","good_evening":"Good evening","good_morning":"Good morning","hello":"Hello","home":"Home","miscellaneous":"Miscellaneous","numbers":"Numbers","off":"Off","on":"On","open":"Open","unclosed":"Unclosed"},"input_select":{"input_selects":"Input Selects"},"light":{"all_lights":"All Lights","lights":"Lights"},"lock":{"locks":"Locks"},"media_player":{"media_players":"Mediaplayers"},"scene":{"scenes":"Scenes"},"select":{"selects":"Selects"},"sensor":{"binary":"Binary","sensors":"Sensors"},"switch":{"all_switches":"All Switches","switches":"Switches"},"vacuum":{"all_vacuums":"All Vacuums","vacuums":"Vacuums"}}');var s=r.t(o,2);const n=JSON.parse('{"camera":{"all_cameras":"Todas las Cámaras","cameras":"Cámaras"},"climate":{"all_climates":"Todos los Termostatos","climates":"Termostatos"},"cover":{"all_covers":"Todas las Cubiertas","covers":"Cubiertas"},"fan":{"all_fans":"Todos los Ventiladores","fans":"Ventiladores"},"generic":{"all":"Todo","areas":"Áreas","busy":"Ocupado","good_afternoon":"Buenas tardes","good_evening":"Buenas noches","good_morning":"Buenos días","hello":"Hola","home":"Inicio","miscellaneous":"Varios","numbers":"Números","off":"Apagado","on":"Encendido","open":"Abierto","unclosed":"Sin Cerrar"},"input_select":{"input_selects":"Selecciones de Entrada"},"light":{"all_lights":"Todas las Luces","lights":"Luces"},"lock":{"locks":"Candados"},"media_player":{"media_players":"Reproductores Multimedia"},"select":{"selects":"Seleccionar"},"sensor":{"binary":"Binario","sensors":"Sensores"},"switch":{"all_switches":"Todos los Apagadores","switches":"Apagadores"},"vacuum":{"all_vacuums":"Todas las Aspiradoras","vacuums":"Aspiradoras"}}');var c=r.t(n,2);const l=JSON.parse('{"camera":{"all_cameras":"Alle Cameras","cameras":"Cameras"},"climate":{"all_climates":"Alle Klimaatregelingen","climates":"Klimaatregelingen"},"cover":{"all_covers":"Alle Bedekkingen","covers":"Bedekkingen"},"fan":{"all_fans":"Alle Ventilatoren","fans":"Ventilatoren"},"generic":{"all":"Alle","areas":"Ruimtes","busy":"Bezig","good_afternoon":"Goede middag","good_evening":"Goede avond","good_morning":"Goede morgen","hello":"Hallo","home":"Start","miscellaneous":"Overige","numbers":"Nummers","off":"Uit","on":"Aan","open":"Open","unclosed":"Ongesloten"},"input_select":{"input_selects":"Lijsten"},"light":{"all_lights":"Alle Lampen","lights":"Lampen"},"lock":{"locks":"Sloten"},"media_player":{"media_players":"Mediaspelers"},"scene":{"scenes":"Scenes"},"select":{"selects":"Statuslijsten"},"sensor":{"binary":"Binaire","sensors":"Sensoren"},"switch":{"all_switches":"Alle Schakelaars","switches":"Schakelaars"},"vacuum":{"all_vacuums":"Alle Afzuiging","vacuums":"Afzuiging"}}');var d=r.t(l,2);const h=JSON.parse('{"camera":{"all_cameras":"Alle Kameras","cameras":"Kameras"},"climate":{"all_climates":"Alle Klimaanlagen","climates":"Klimaanlagen"},"cover":{"all_covers":"Alle Abdeckungen","covers":"Abdeckungen"},"fan":{"all_fans":"Alle Ventilatoren","fans":"Ventilatoren"},"generic":{"all":"Alle","areas":"Bereiche","busy":"Beschäftigt","good_afternoon":"Guten Nachmittag","good_evening":"Guten Abend","good_morning":"Guten Morgen","hello":"Hallo","home":"Start","miscellaneous":"Sonstiges","numbers":"Zahlen","off":"Aus","on":"Ein","open":"Offen","unclosed":"Nicht Geschlossen"},"input_select":{"input_selects":"Auswahl-Eingaben"},"light":{"all_lights":"Alle Leuchten","lights":"Leuchten"},"lock":{"locks":"Schlösser"},"media_player":{"media_players":"Wiedergabegeräte"},"scene":{"scenes":"Szenen"},"select":{"selects":"Auswahlen"},"sensor":{"binary":"Binäre","sensors":"Sensoren"},"switch":{"all_switches":"Alle Schalter","switches":"Schalter"},"vacuum":{"all_vacuums":"Alle Staubsauger","vacuums":"Staubsauger"}}'),u={en:s,es:c,nl:d,de:r.t(h,2)};function f(e,t){try{return e.split(".").reduce(((e,t)=>e[t]),u[t])}catch(e){return}}var p,m,w,g,v,y,_,b,C,O,E=r(107),T=function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)},H=function(e,t,r,i,a){if("m"===i)throw new TypeError("Private method is not writable");if("a"===i&&!a)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof t?e!==t||!a:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return"a"===i?a.call(e,r):a?a.value=r:t.set(e,r),r};class j{constructor(){throw new Error("This class should be invoked with method initialize() instead of using the keyword new!")}static get strategyOptions(){return T(this,p,"f",_)}static get areas(){return T(this,p,"f",g)}static get devices(){return T(this,p,"f",w)}static get entities(){return T(this,p,"f",m)}static get debug(){return T(this,p,"f",b)}static async initialize(e){var t;this.customLocalize=(t=e.hass,function(e){let r=f(e,t?.locale.language??"en");return r||(r=f(e,"en")),r??e});const r={areas:{undisclosed:{area_id:"undisclosed",floor_id:null,name:"Undisclosed",picture:null,icon:"mdi:floor-plan",labels:[],aliases:[],hidden:!1}},debug:!1,domains:{_:{hide_config_entities:!0,hide_diagnostic_entities:!0},default:{title:(i=this.customLocalize)("generic.miscellaneous"),showControls:!1,hidden:!1},light:{title:i("light.lights"),showControls:!0,iconOn:"mdi:lightbulb",iconOff:"mdi:lightbulb-off",onService:"light.turn_on",offService:"light.turn_off",hidden:!1},scene:{title:i("scene.scenes"),showControls:!1,onService:"scene.turn_on",hidden:!1},fan:{title:i("fan.fans"),showControls:!0,iconOn:"mdi:fan",iconOff:"mdi:fan-off",onService:"fan.turn_on",offService:"fan.turn_off",hidden:!1},cover:{title:i("cover.covers"),showControls:!0,iconOn:"mdi:arrow-up",iconOff:"mdi:arrow-down",onService:"cover.open_cover",offService:"cover.close_cover",hidden:!1},switch:{title:i("switch.switches"),showControls:!0,iconOn:"mdi:power-plug",iconOff:"mdi:power-plug-off",onService:"switch.turn_on",offService:"switch.turn_off",hidden:!1},camera:{title:i("camera.cameras"),showControls:!1,hidden:!1},lock:{title:i("lock.locks"),showControls:!1,hidden:!1},climate:{title:i("climate.climates"),showControls:!1,hidden:!1},media_player:{title:i("media_player.media_players"),showControls:!1,hidden:!1},sensor:{title:i("sensor.sensors"),showControls:!1,hidden:!1},binary_sensor:{title:`${i("sensor.binary")} `+i("sensor.sensors"),showControls:!1,hidden:!1},number:{title:i("generic.numbers"),showControls:!1,hidden:!1},vacuum:{title:i("vacuum.vacuums"),showControls:!0,hidden:!1},select:{title:i("select.selects"),showControls:!1,hidden:!1},input_select:{title:i("input_select.input_selects"),showControls:!1,hidden:!1}},home_view:{hidden:[]},views:{home:{order:1,hidden:!1},light:{order:2,hidden:!1},fan:{order:3,hidden:!1},cover:{order:4,hidden:!1},switch:{order:5,hidden:!1},climate:{order:6,hidden:!1},camera:{order:7,hidden:!1},vacuum:{order:8,hidden:!1},scene:{order:9,hidden:!1}}};var i;H(this,p,a()(r,e.config?.strategy?.options??{}),"f",_),H(this,p,e.hass.states,"f",v),H(this,p,T(this,p,"f",_).debug,"f",b);try{[{set value(e){H(p,p,e,"f",m)}}.value,{set value(e){H(p,p,e,"f",w)}}.value,{set value(e){H(p,p,e,"f",g)}}.value]=await Promise.all([e.hass.callWS({type:"config/entity_registry/list"}),e.hass.callWS({type:"config/device_registry/list"}),e.hass.callWS({type:"config/area_registry/list"})])}catch(e){throw p.logError("An error occurred while querying Home assistant's registries!",e),"Check the console for details"}T(this,p,"f",_).areas.undisclosed?.hidden||(T(this,p,"f",_).areas.undisclosed={...r.areas.undisclosed,...T(this,p,"f",_).areas.undisclosed},T(this,p,"f",_).areas.undisclosed.area_id="undisclosed",T(this,p,"f",g).push(T(this,p,"f",_).areas.undisclosed)),H(this,p,p.areas.map((e=>({...e,...T(this,p,"f",_).areas?.[e.area_id]}))),"f",g),T(this,p,"f",g).sort(((e,t)=>(e.order??1/0)-(t.order??1/0)||e.name.localeCompare(t.name))),T(this,p,"f",_).views=Object.fromEntries(Object.entries(T(this,p,"f",_).views).sort((([,e],[,t])=>(e.order??1/0)-(t.order??1/0)||(e.title??"undefined").localeCompare(t.title??"undefined")))),T(this,p,"f",_).domains=Object.fromEntries(Object.entries(T(this,p,"f",_).domains).sort((([,e],[,t])=>(e.order??1/0)-(t.order??1/0)||(e.title??"undefined").localeCompare(t.title??"undefined")))),H(this,p,!0,"f",y)}static isInitialized(){return T(this,p,"f",y)}static getCountTemplate(e,t,r){const i=[];this.isInitialized()||console.warn("Helper class should be initialized before calling this method!");for(const t of T(this,p,"f",g)){let r=this.getDeviceEntities(t,e);r=(0,E.j)(r,e);const a=r.map((e=>`states['${e.entity_id}']`));i.push(...a)}return`{% set entities = [${i}] %}\n {{ entities\n | selectattr('state','${t}','${r}')\n | selectattr('state','ne','unavailable')\n | selectattr('state','ne','unknown')\n | list\n | count\n }}`}static getDeviceEntities(e,t){this.isInitialized()||console.warn("Helper class should be initialized before calling this method!");const r=T(this,p,"f",w).filter((t=>(t.area_id??"undisclosed")===e.area_id)).map((e=>e.id));return T(this,p,"f",m).filter(T(this,p,"m",C),{area:e,domain:t,areaDeviceIds:r}).sort(((e,t)=>(e.original_name??"undefined").localeCompare(t.original_name??"undefined")))}static getStateEntities(e,t){this.isInitialized()||console.warn("Helper class should be initialized before calling this method!");const r=[],i=Object.fromEntries(T(this,p,"f",m).map((e=>[e.entity_id,e]))),a=Object.fromEntries(T(this,p,"f",w).map((e=>[e.id,e]))),o=Object.values(T(this,p,"f",v)).filter((e=>e.entity_id.startsWith(`${t}.`)));for(const t of o){const o=i[t.entity_id],s=a[o?.device_id??""];(o?.area_id===e.area_id||s&&s.area_id===e.area_id)&&r.push(t)}return r}static getEntityState(e){return T(this,p,"f",v)[e.entity_id]}static sanitizeClassName(e){return(e=e.charAt(0).toUpperCase()+e.slice(1)).replace(/([-_][a-z])/g,(e=>e.toUpperCase().replace("-","").replace("_","")))}static getExposedViewIds(){return this.isInitialized()||console.warn("Helper class should be initialized before calling this method!"),T(this,p,"m",O).call(this,T(this,p,"f",_).views,"hidden",!1)}static getExposedDomainIds(){return this.isInitialized()||console.warn("Helper class should be initialized before calling this method!"),T(this,p,"m",O).call(this,T(this,p,"f",_).domains,"hidden",!1)}static logError(e,t){p.debug?console.error(e,t):console.error(e)}}p=j,C=function(e){const t=p.strategyOptions.card_options?.[e.entity_id],r=p.strategyOptions.card_options?.[e.device_id??"null"],i=!t?.hidden&&!r?.hidden&&null===e.hidden_by&&null===e.disabled_by,a=void 0===this.domain||e.entity_id.startsWith(`${this.domain}.`),o="undisclosed"===this.area.area_id?!e.area_id&&(this.areaDeviceIds.includes(e.device_id??"")||!e.device_id):e.area_id===this.area.area_id||!e.area_id&&this.areaDeviceIds.includes(e.device_id??"");return i&&a&&o},O=function(e,t,r){const i=[];for(const a of Object.keys(e))e[a][t]===r&&i.push(a);return i},m={value:void 0},w={value:void 0},g={value:[]},v={value:void 0},y={value:!1},_={value:void 0},b={value:void 0}},818:(e,t,r)=>{"use strict";r.r(t),r.d(t,{AbstractCard:()=>a});var i=r(245);class a{constructor(e){if(this.config={type:"custom:mushroom-entity-card",icon:"mdi:help-circle"},!i.H.isInitialized())throw new Error("The Helper module must be initialized before using this one.");this.entity=e}getCard(){return{...this.config,entity:"entity_id"in this.entity?this.entity.entity_id:void 0}}}},301:(e,t,r)=>{"use strict";r.r(t),r.d(t,{AreaCard:()=>s});var i,a=r(818),o=function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)};class s extends a.AbstractCard{constructor(e,t={}){super(e),i.set(this,{type:"custom:mushroom-template-card",primary:void 0,icon:"mdi:floor-plan",icon_color:"blue",tap_action:{action:"navigate",navigation_path:""},hold_action:{action:"none"}}),"default"===t.type&&delete t.type,o(this,i,"f").primary=e.name,o(this,i,"f").tap_action&&"navigation_path"in o(this,i,"f").tap_action&&(o(this,i,"f").tap_action.navigation_path=e.area_id),e.icon&&(o(this,i,"f").icon=e.icon),this.config=Object.assign(this.config,o(this,i,"f"),t)}}i=new WeakMap},151:(e,t,r)=>{"use strict";r.r(t),r.d(t,{BinarySensorCard:()=>o});var i,a=r(982);class o extends a.SensorCard{constructor(e,t={}){super(e),i.set(this,{type:"custom:mushroom-entity-card",icon:"mdi:power-cycle",icon_color:"green"}),this.config=Object.assign(this.config,function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)}(this,i,"f"),t)}}i=new WeakMap},723:(e,t,r)=>{"use strict";r.r(t),r.d(t,{CameraCard:()=>o});var i,a=r(818);class o extends a.AbstractCard{constructor(e,t={}){super(e),i.set(this,{entity:"",type:"picture-entity",show_name:!1,show_state:!1,camera_view:"live"}),this.config=Object.assign(this.config,function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)}(this,i,"f"),t)}}i=new WeakMap},721:(e,t,r)=>{"use strict";r.r(t),r.d(t,{ClimateCard:()=>o});var i,a=r(818);class o extends a.AbstractCard{constructor(e,t={}){super(e),i.set(this,{type:"custom:mushroom-climate-card",icon:void 0,hvac_modes:["off","cool","heat","fan_only"],show_temperature_control:!0}),this.config=Object.assign(this.config,function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)}(this,i,"f"),t)}}i=new WeakMap},896:(e,t,r)=>{"use strict";r.r(t),r.d(t,{ControllerCard:()=>n});var i,a,o=function(e,t,r,i,a){if("m"===i)throw new TypeError("Private method is not writable");if("a"===i&&!a)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof t?e!==t||!a:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return"a"===i?a.call(e,r):a?a.value=r:t.set(e,r),r},s=function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)};class n{constructor(e,t={}){i.set(this,void 0),a.set(this,{type:"mushroom-title-card",showControls:!0,iconOn:"mdi:power-on",iconOff:"mdi:power-off",onService:"none",offService:"none"}),o(this,i,e,"f"),o(this,a,{...s(this,a,"f"),...t},"f")}createCard(){const e=[{type:"custom:mushroom-title-card",title:s(this,a,"f").title,subtitle:s(this,a,"f").subtitle}];return s(this,a,"f").showControls&&e.push({type:"horizontal-stack",cards:[{type:"custom:mushroom-template-card",icon:s(this,a,"f").iconOff,layout:"vertical",icon_color:"red",tap_action:{action:"call-service",service:s(this,a,"f").offService,target:s(this,i,"f"),data:{}}},{type:"custom:mushroom-template-card",icon:s(this,a,"f").iconOn,layout:"vertical",icon_color:"amber",tap_action:{action:"call-service",service:s(this,a,"f").onService,target:s(this,i,"f"),data:{}}}]}),{type:"horizontal-stack",cards:e}}}i=new WeakMap,a=new WeakMap},33:(e,t,r)=>{"use strict";r.r(t),r.d(t,{CoverCard:()=>o});var i,a=r(818);class o extends a.AbstractCard{constructor(e,t={}){super(e),i.set(this,{type:"custom:mushroom-cover-card",icon:void 0,show_buttons_control:!0,show_position_control:!0,show_tilt_position_control:!0}),this.config=Object.assign(this.config,function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)}(this,i,"f"),t)}}i=new WeakMap},667:(e,t,r)=>{"use strict";r.r(t),r.d(t,{FanCard:()=>o});var i,a=r(818);class o extends a.AbstractCard{constructor(e,t={}){super(e),i.set(this,{type:"custom:mushroom-fan-card",icon:void 0,show_percentage_control:!0,show_oscillate_control:!0,icon_animation:!0}),this.config=Object.assign(this.config,function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)}(this,i,"f"),t)}}i=new WeakMap},42:(e,t,r)=>{"use strict";r.r(t),r.d(t,{AreaCard:()=>s});var i,a=r(818),o=function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)};class s extends a.AbstractCard{constructor(e,t={}){super(e),i.set(this,{type:"area",area:""}),o(this,i,"f").area=e.area_id,o(this,i,"f").navigation_path=o(this,i,"f").area,delete t.type,this.config=Object.assign(this.config,o(this,i,"f"),t)}}i=new WeakMap},810:(e,t,r)=>{"use strict";r.r(t),r.d(t,{InputSelectCard:()=>o});var i,a=r(212);class o extends a.SelectCard{constructor(e,t={}){super(e),i.set(this,{type:"custom:mushroom-select-card",icon:void 0}),this.config=Object.assign(this.config,function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)}(this,i,"f"),t)}}i=new WeakMap},254:(e,t,r)=>{"use strict";r.r(t),r.d(t,{LightCard:()=>l});var i,a=r(818),o=r(843),s=function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)},n=o.M.isCallServiceActionConfig,c=o.M.isCallServiceActionTarget;class l extends a.AbstractCard{constructor(e,t={}){super(e),i.set(this,{type:"custom:mushroom-light-card",icon:void 0,show_brightness_control:!0,show_color_control:!0,show_color_temp_control:!0,use_light_color:!0,double_tap_action:{action:"call-service",service:"light.turn_on",target:{entity_id:void 0},data:{rgb_color:[255,255,255]}}}),n(s(this,i,"f").double_tap_action)&&c(s(this,i,"f").double_tap_action.target)&&(s(this,i,"f").double_tap_action.target.entity_id=e.entity_id),this.config=Object.assign(this.config,s(this,i,"f"),t)}}i=new WeakMap},936:(e,t,r)=>{"use strict";r.r(t),r.d(t,{LockCard:()=>o});var i,a=r(818);class o extends a.AbstractCard{constructor(e,t={}){super(e),i.set(this,{type:"custom:mushroom-lock-card",icon:void 0}),this.config=Object.assign(this.config,function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)}(this,i,"f"),t)}}i=new WeakMap},879:(e,t,r)=>{"use strict";r.r(t),r.d(t,{MediaPlayerCard:()=>o});var i,a=r(818);class o extends a.AbstractCard{constructor(e,t={}){super(e),i.set(this,{type:"custom:mushroom-media-player-card",use_media_info:!0,media_controls:["on_off","play_pause_stop"],show_volume_level:!0,volume_controls:["volume_mute","volume_set","volume_buttons"]}),this.config=Object.assign(this.config,function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)}(this,i,"f"),t)}}i=new WeakMap},486:(e,t,r)=>{"use strict";r.r(t),r.d(t,{MiscellaneousCard:()=>o});var i,a=r(818);class o extends a.AbstractCard{constructor(e,t={}){super(e),i.set(this,{type:"custom:mushroom-entity-card",icon_color:"blue-grey"}),this.config=Object.assign(this.config,function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)}(this,i,"f"),t)}}i=new WeakMap},137:(e,t,r)=>{"use strict";r.r(t),r.d(t,{NumberCard:()=>o});var i,a=r(818);class o extends a.AbstractCard{constructor(e,t={}){super(e),i.set(this,{type:"custom:mushroom-number-card",icon:void 0}),this.config=Object.assign(this.config,function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)}(this,i,"f"),t)}}i=new WeakMap},845:(e,t,r)=>{"use strict";r.r(t),r.d(t,{PersonCard:()=>o});var i,a=r(818);class o extends a.AbstractCard{constructor(e,t={}){super(e),i.set(this,{type:"custom:mushroom-person-card",layout:"vertical",primary_info:"none",secondary_info:"none",icon_type:"entity-picture"}),this.config=Object.assign(this.config,function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)}(this,i,"f"),t)}}i=new WeakMap},858:(e,t,r)=>{"use strict";r.r(t),r.d(t,{SceneCard:()=>d});var i,a=r(818),o=r(843),s=r(245),n=function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)},c=o.M.isCallServiceActionConfig,l=o.M.isCallServiceActionTarget;class d extends a.AbstractCard{constructor(e,t={}){super(e),i.set(this,{type:"custom:mushroom-entity-card",icon:"mdi:palette",icon_color:"blue",tap_action:{action:"call-service",service:"scene.turn_on",target:{entity_id:void 0}}}),c(n(this,i,"f").tap_action)&&l(n(this,i,"f").tap_action.target)&&(n(this,i,"f").tap_action.target.entity_id=e.entity_id),n(this,i,"f").icon=s.H.getEntityState(e)?.attributes.icon??n(this,i,"f").icon,this.config=Object.assign(this.config,n(this,i,"f"),t)}}i=new WeakMap},212:(e,t,r)=>{"use strict";r.r(t),r.d(t,{SelectCard:()=>o});var i,a=r(818);class o extends a.AbstractCard{constructor(e,t={}){super(e),i.set(this,{type:"custom:mushroom-select-card",icon:void 0}),this.config=Object.assign(this.config,function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)}(this,i,"f"),t)}}i=new WeakMap},982:(e,t,r)=>{"use strict";r.r(t),r.d(t,{SensorCard:()=>o});var i,a=r(818);class o extends a.AbstractCard{constructor(e,t={}){super(e),i.set(this,{type:"custom:mushroom-entity-card",icon:"mdi:information",animate:!0,line_color:"green"}),this.config=Object.assign(this.config,function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)}(this,i,"f"),t)}}i=new WeakMap},708:(e,t,r)=>{"use strict";r.r(t),r.d(t,{SwitchCard:()=>o});var i,a=r(818);class o extends a.AbstractCard{constructor(e,t={}){super(e),i.set(this,{type:"custom:mushroom-entity-card",icon:void 0,tap_action:{action:"toggle"}}),this.config=Object.assign(this.config,function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)}(this,i,"f"),t)}}i=new WeakMap},430:(e,t,r)=>{"use strict";r.r(t),r.d(t,{VacuumCard:()=>s});var i=r(818);const a=["on_off","start_pause","stop","locate","clean_spot","return_home"];var o;class s extends i.AbstractCard{constructor(e,t={}){super(e),o.set(this,{type:"custom:mushroom-vacuum-card",icon:void 0,icon_animation:!0,commands:[...a],tap_action:{action:"more-info"}}),this.config=Object.assign(this.config,function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)}(this,o,"f"),t)}}o=new WeakMap},222:(e,t,r)=>{"use strict";r.r(t),r.d(t,{AbstractChip:()=>o});var i=r(245),a=r(843).M.isCallServiceActionConfig;class o{constructor(){if(this.config={type:"template"},!i.H.isInitialized())throw new Error("The Helper module must be initialized before using this one.")}getChip(){return this.config}setTapActionTarget(e){"tap_action"in this.config&&a(this.config.tap_action)?this.config.tap_action.target=e:i.H.debug&&console.warn(this.constructor.name+" - Target not set: Invalid target or tap action.")}}},757:(e,t,r)=>{"use strict";r.r(t),r.d(t,{ClimateChip:()=>s});var i,a=r(245),o=r(222);class s extends o.AbstractChip{constructor(e={}){super(),i.set(this,{type:"template",icon:"mdi:thermostat",icon_color:"orange",content:a.H.getCountTemplate("climate","ne","off"),tap_action:{action:"none"},hold_action:{action:"navigate",navigation_path:"climates"}}),this.config=Object.assign(this.config,function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)}(this,i,"f"),e)}}i=new WeakMap},589:(e,t,r)=>{"use strict";r.r(t),r.d(t,{CoverChip:()=>s});var i,a=r(245),o=r(222);class s extends o.AbstractChip{constructor(e={}){super(),i.set(this,{type:"template",icon:"mdi:window-open",icon_color:"cyan",content:a.H.getCountTemplate("cover","search","(open|opening|closing)"),tap_action:{action:"none"},hold_action:{action:"navigate",navigation_path:"covers"}}),this.config=Object.assign(this.config,function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)}(this,i,"f"),e)}}i=new WeakMap},867:(e,t,r)=>{"use strict";r.r(t),r.d(t,{FanChip:()=>s});var i,a=r(245),o=r(222);class s extends o.AbstractChip{constructor(e={}){super(),i.set(this,{type:"template",icon:"mdi:fan",icon_color:"green",content:a.H.getCountTemplate("fan","eq","on"),tap_action:{action:"call-service",service:"fan.turn_off"},hold_action:{action:"navigate",navigation_path:"fans"}}),this.config=Object.assign(this.config,function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)}(this,i,"f"),e)}}i=new WeakMap},741:(e,t,r)=>{"use strict";r.r(t),r.d(t,{LightChip:()=>s});var i,a=r(245),o=r(222);class s extends o.AbstractChip{constructor(e={}){super(),i.set(this,{type:"template",icon:"mdi:lightbulb-group",icon_color:"amber",content:a.H.getCountTemplate("light","eq","on"),tap_action:{action:"call-service",service:"light.turn_off"},hold_action:{action:"navigate",navigation_path:"lights"}}),this.config=Object.assign(this.config,function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)}(this,i,"f"),e)}}i=new WeakMap},908:(e,t,r)=>{"use strict";r.r(t),r.d(t,{SwitchChip:()=>s});var i,a=r(245),o=r(222);class s extends o.AbstractChip{constructor(e={}){super(),i.set(this,{type:"template",icon:"mdi:dip-switch",icon_color:"blue",content:a.H.getCountTemplate("switch","eq","on"),tap_action:{action:"call-service",service:"switch.turn_off"},hold_action:{action:"navigate",navigation_path:"switches"}}),this.config=Object.assign(this.config,function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)}(this,i,"f"),e)}}i=new WeakMap},778:(e,t,r)=>{"use strict";r.r(t),r.d(t,{WeatherChip:()=>s});var i,a=r(222),o=function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)};class s extends a.AbstractChip{constructor(e,t={}){super(),i.set(this,{type:"weather",show_temperature:!0,show_conditions:!0}),function(e,t,r,i,a){if("m"===i)throw new TypeError("Private method is not writable");if("a"===i&&!a)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof t?e!==t||!a:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");"a"===i?a.call(e,r):a?a.value=r:t.set(e,r)}(this,i,{...o(this,i,"f"),entity:e,...t},"f"),this.config=Object.assign(this.config,o(this,i,"f"),t)}}i=new WeakMap},843:(e,t,r)=>{"use strict";var i;r.d(t,{M:()=>i}),function(e){e.isCallServiceActionConfig=function(e){return e&&"call-service"===e.action&&["action","service"].every((t=>t in e))},e.isCallServiceActionTarget=function(e){return e&&["entity_id","device_id","area_id"].some((t=>t in e))}}(i||(i={}))},107:(e,t,r)=>{"use strict";r.d(t,{j:()=>o});var i=r(245);function a(e,t,r,i=!0){return e.filter((e=>i?e[t]!==r:e[t]===r))}function o(e,t){if(!i.H.isInitialized())throw new Error("The Helper module must be initialized before using this one.");const r={...i.H.strategyOptions.domains._,...i.H.strategyOptions.domains[t]};let o=[];return r.hide_config_entities&&(e=a(e,"entity_category","config"),o.push("Config")),r.hide_diagnostic_entities&&(e=a(e,"entity_category","diagnostic"),o.push("Diagnostic")),i.H.debug&&o.length>0&&console.warn(o.join(" & ")+" entities are filtered out."),e}},122:(e,t,r)=>{"use strict";r.r(t),r.d(t,{AbstractView:()=>c});var i,a=r(245),o=r(896),s=r(107),n=function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)};class c{constructor(e){if(this.config={icon:"mdi:view-dashboard",subview:!1},this.viewControllerCard={cards:[],type:""},i.set(this,void 0),!a.H.isInitialized())throw new Error("The Helper module must be initialized before using this one.");!function(e,t,r,i,a){if("m"===i)throw new TypeError("Private method is not writable");if("a"===i&&!a)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof t?e!==t||!a:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");"a"===i?a.call(e,r):a?a.value=r:t.set(e,r)}(this,i,e,"f")}async createViewCards(){const e=[];for(const t of a.H.areas){const c=[],l=a.H.sanitizeClassName(n(this,i,"f")+"Card"),d=await r(560)(`./${l}`);let h={area_id:[t.area_id]},u=a.H.getDeviceEntities(t,n(this,i,"f"));u=(0,s.j)(u,n(this,i,"f")),"undisclosed"===t.area_id&&(h={entity_id:u.map((e=>e.entity_id))});for(const e of u){let t=a.H.strategyOptions.card_options?.[e.entity_id],r=a.H.strategyOptions.card_options?.[e.device_id??"null"];t?.hidden||r?.hidden||c.push(new d[l](e,t).getCard())}if(c.length){const r="controllerCardOptions"in this.config?this.config.controllerCardOptions:{};c.unshift(new o.ControllerCard(h,Object.assign({title:t.name},r)).createCard()),e.push({type:"vertical-stack",cards:c})}}return this.viewControllerCard.cards.length&&e.length&&e.unshift(this.viewControllerCard),e}async getView(){return{...this.config,cards:await this.createViewCards()}}targetDomain(e){return{entity_id:a.H.entities.filter((t=>t.entity_id.startsWith(e+".")&&!t.hidden_by&&!a.H.strategyOptions.card_options?.[t.entity_id]?.hidden)).map((e=>e.entity_id))}}}i=new WeakMap},255:(e,t,r)=>{"use strict";r.r(t),r.d(t,{CameraView:()=>h});var i,a,o,s,n=r(896),c=r(122),l=r(245),d=function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)};class h extends c.AbstractView{constructor(e={}){super(d(i,i,"f",a)),o.set(this,{title:l.H.customLocalize("camera.cameras"),path:"cameras",icon:"mdi:cctv",subview:!1,controllerCardOptions:{showControls:!1}}),s.set(this,{title:l.H.customLocalize("camera.all_cameras"),subtitle:`${l.H.getCountTemplate(d(i,i,"f",a),"ne","off")} ${l.H.customLocalize("camera.cameras")} `+l.H.customLocalize("generic.busy")}),this.config=Object.assign(this.config,d(this,o,"f"),e),this.viewControllerCard=new n.ControllerCard({},{...d(this,s,"f"),..."controllerCardOptions"in this.config?this.config.controllerCardOptions:{}}).createCard()}}i=h,o=new WeakMap,s=new WeakMap,a={value:"camera"}},559:(e,t,r)=>{"use strict";r.r(t),r.d(t,{ClimateView:()=>h});var i,a,o,s,n=r(245),c=r(896),l=r(122),d=function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)};class h extends l.AbstractView{constructor(e={}){super(d(i,i,"f",a)),o.set(this,{title:n.H.customLocalize("climate.climates"),path:"climates",icon:"mdi:thermostat",subview:!1,controllerCardOptions:{showControls:!1}}),s.set(this,{title:n.H.customLocalize("climate.all_climates"),subtitle:`${n.H.getCountTemplate(d(i,i,"f",a),"ne","off")} ${n.H.customLocalize("climate.climates")} `+n.H.customLocalize("generic.busy")}),this.config=Object.assign(this.config,d(this,o,"f"),e),this.viewControllerCard=new c.ControllerCard(this.targetDomain(d(i,i,"f",a)),{...d(this,s,"f"),..."controllerCardOptions"in this.config?this.config.controllerCardOptions:{}}).createCard()}}i=h,o=new WeakMap,s=new WeakMap,a={value:"climate"}},547:(e,t,r)=>{"use strict";r.r(t),r.d(t,{CoverView:()=>h});var i,a,o,s,n=r(245),c=r(896),l=r(122),d=function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)};class h extends l.AbstractView{constructor(e={}){super(d(i,i,"f",a)),o.set(this,{title:n.H.customLocalize("cover.covers"),path:"covers",icon:"mdi:window-open",subview:!1,controllerCardOptions:{iconOn:"mdi:arrow-up",iconOff:"mdi:arrow-down",onService:"cover.open_cover",offService:"cover.close_cover"}}),s.set(this,{title:n.H.customLocalize("cover.all_covers"),subtitle:`${n.H.getCountTemplate(d(i,i,"f",a),"search","(open|opening|closing)")} ${n.H.customLocalize("cover.covers")} `+n.H.customLocalize("generic.unclosed")}),this.config=Object.assign(this.config,d(this,o,"f"),e),this.viewControllerCard=new c.ControllerCard(this.targetDomain(d(i,i,"f",a)),{...d(this,s,"f"),..."controllerCardOptions"in this.config?this.config.controllerCardOptions:{}}).createCard()}}i=h,o=new WeakMap,s=new WeakMap,a={value:"cover"}},626:(e,t,r)=>{"use strict";r.r(t),r.d(t,{FanView:()=>h});var i,a,o,s,n=r(245),c=r(896),l=r(122),d=function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)};class h extends l.AbstractView{constructor(e={}){super(d(i,i,"f",a)),o.set(this,{title:n.H.customLocalize("fan.fans"),path:"fans",icon:"mdi:fan",subview:!1,controllerCardOptions:{iconOn:"mdi:fan",iconOff:"mdi:fan-off",onService:"fan.turn_on",offService:"fan.turn_off"}}),s.set(this,{title:n.H.customLocalize("fan.all_fans"),subtitle:`${n.H.getCountTemplate(d(i,i,"f",a),"eq","on")} ${n.H.customLocalize("fan.fans")} `+n.H.customLocalize("generic.on")}),this.config=Object.assign(this.config,d(this,o,"f"),e),this.viewControllerCard=new c.ControllerCard(this.targetDomain(d(i,i,"f",a)),{...d(this,s,"f"),..."controllerCardOptions"in this.config?this.config.controllerCardOptions:{}}).createCard()}}i=h,o=new WeakMap,s=new WeakMap,a={value:"fan"}},31:(e,t,r)=>{"use strict";r.r(t),r.d(t,{HomeView:()=>h});var i,a,o,s,n,c=r(245),l=r(122),d=function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)};class h extends l.AbstractView{constructor(e={}){super("home"),i.add(this),a.set(this,{title:c.H.customLocalize("generic.home"),icon:"mdi:home-assistant",path:"home",subview:!1}),this.config=Object.assign(this.config,d(this,a,"f"),e)}async createViewCards(){return await Promise.all([d(this,i,"m",o).call(this),d(this,i,"m",s).call(this),d(this,i,"m",n).call(this)]).then((([e,t,r])=>{const i=c.H.strategyOptions,a=[];return e.length&&a.push({type:"custom:mushroom-chips-card",alignment:"center",chips:e}),t.length&&a.push({type:"horizontal-stack",cards:t}),c.H.strategyOptions.home_view.hidden.includes("greeting")||a.push({type:"custom:mushroom-template-card",primary:`{% set time = now().hour %} {% if (time >= 18) %} ${c.H.customLocalize("generic.good_evening")},{{user}}!\n {% elif (time >= 12) %} ${c.H.customLocalize("generic.good_afternoon")}, {{user}}!\n {% elif (time >= 5) %} ${c.H.customLocalize("generic.good_morning")}, {{user}}!\n {% else %} ${c.H.customLocalize("generic.hello")}, {{user}}! {% endif %}`,icon:"mdi:hand-wave",icon_color:"orange",tap_action:{action:"none"},double_tap_action:{action:"none"},hold_action:{action:"none"}}),i.quick_access_cards&&a.push(...i.quick_access_cards),a.push({type:"vertical-stack",cards:r}),i.extra_cards&&a.push(...i.extra_cards),a}))}}a=new WeakMap,i=new WeakSet,o=async function(){if(c.H.strategyOptions.home_view.hidden.includes("chips"))return[];const e=[],t=c.H.strategyOptions.chips,i=["light","fan","cover","switch","climate"],a=c.H.areas.map((e=>e.area_id??""));let o;const s=t?.weather_entity??c.H.entities.find((e=>e.entity_id.startsWith("weather.")&&null===e.disabled_by&&null===e.hidden_by))?.entity_id;if(s)try{o=await Promise.resolve().then(r.bind(r,778));const t=new o.WeatherChip(s);e.push(t.getChip())}catch(e){c.H.logError("An error occurred while creating the weather chip!",e)}for(let s of i)if(t?.[`${s}_count`]??1){const t=c.H.sanitizeClassName(s+"Chip");try{o=await r(216)(`./${t}`);const i=new o[t];i.setTapActionTarget({area_id:a}),e.push(i.getChip())}catch(e){c.H.logError(`An error occurred while creating the ${s} chip!`,e)}}return t?.extra_chips&&e.push(...t.extra_chips),e},s=function(){if(c.H.strategyOptions.home_view.hidden.includes("persons"))return[];const e=[];return Promise.resolve().then(r.bind(r,845)).then((t=>{for(const r of c.H.entities.filter((e=>e.entity_id.startsWith("person.")&&null==e.hidden_by&&null==e.disabled_by)))e.push(new t.PersonCard(r).getCard())})),e},n=async function(){if(c.H.strategyOptions.home_view.hidden.includes("areas"))return[];const e=[];let t=[];c.H.strategyOptions.home_view.hidden.includes("areasTitle")||e.push({type:"custom:mushroom-title-card",title:c.H.customLocalize("generic.areas")});for(const[i,a]of c.H.areas.entries()){let o,s=c.H.strategyOptions.areas[a.area_id]?.type??c.H.strategyOptions.areas._?.type??"default";try{o=await r(560)(`./${s}`)}catch(e){o=await Promise.resolve().then(r.bind(r,301)),c.H.strategyOptions.debug&&"default"!==s&&console.error(e)}if(!c.H.strategyOptions.areas[a.area_id]?.hidden){let e={...c.H.strategyOptions.areas._,...c.H.strategyOptions.areas[a.area_id]};t.push(new o.AreaCard(a,e).getCard())}if(i===c.H.areas.length-1)for(let r=0;r{"use strict";r.r(t),r.d(t,{LightView:()=>h});var i,a,o,s,n=r(245),c=r(896),l=r(122),d=function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)};class h extends l.AbstractView{constructor(e={}){super(d(i,i,"f",a)),o.set(this,{title:n.H.customLocalize("light.lights"),path:"lights",icon:"mdi:lightbulb-group",subview:!1,controllerCardOptions:{iconOn:"mdi:lightbulb",iconOff:"mdi:lightbulb-off",onService:"light.turn_on",offService:"light.turn_off"}}),s.set(this,{title:n.H.customLocalize("light.all_lights"),subtitle:`${n.H.getCountTemplate(d(i,i,"f",a),"eq","on")} ${n.H.customLocalize("light.lights")} `+n.H.customLocalize("generic.on")}),this.config=Object.assign(this.config,d(this,o,"f"),e),this.viewControllerCard=new c.ControllerCard(this.targetDomain(d(i,i,"f",a)),{...d(this,s,"f"),..."controllerCardOptions"in this.config?this.config.controllerCardOptions:{}}).createCard()}}i=h,o=new WeakMap,s=new WeakMap,a={value:"light"}},204:(e,t,r)=>{"use strict";r.r(t),r.d(t,{SceneView:()=>l});var i,a,o,s=r(245),n=r(122),c=function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)};class l extends n.AbstractView{constructor(e={}){super(c(i,i,"f",a)),o.set(this,{title:s.H.customLocalize("scene.scenes"),path:"scenes",icon:"mdi:palette",subview:!1,controllerCardOptions:{showControls:!1}}),this.config=Object.assign(this.config,c(this,o,"f"),e)}}i=l,o=new WeakMap,a={value:"scene"}},464:(e,t,r)=>{"use strict";r.r(t),r.d(t,{SwitchView:()=>h});var i,a,o,s,n=r(245),c=r(896),l=r(122),d=function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)};class h extends l.AbstractView{constructor(e={}){super(d(i,i,"f",a)),o.set(this,{title:n.H.customLocalize("switch.switches"),path:"switches",icon:"mdi:dip-switch",subview:!1,controllerCardOptions:{iconOn:"mdi:power-plug",iconOff:"mdi:power-plug-off",onService:"switch.turn_on",offService:"switch.turn_off"}}),s.set(this,{title:n.H.customLocalize("switch.all_switches"),subtitle:`${n.H.getCountTemplate(d(i,i,"f",a),"eq","on")} ${n.H.customLocalize("switch.switches")} `+n.H.customLocalize("generic.on")}),this.config=Object.assign(this.config,d(this,o,"f"),e),this.viewControllerCard=new c.ControllerCard(this.targetDomain(d(i,i,"f",a)),{...d(this,s,"f"),..."controllerCardOptions"in this.config?this.config.controllerCardOptions:{}}).createCard()}}i=h,o=new WeakMap,s=new WeakMap,a={value:"switch"}},499:(e,t,r)=>{"use strict";r.r(t),r.d(t,{VacuumView:()=>h});var i,a,o,s,n=r(245),c=r(896),l=r(122),d=function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)};class h extends l.AbstractView{constructor(e={}){super(d(i,i,"f",a)),o.set(this,{title:n.H.customLocalize("vacuum.vacuums"),path:"vacuums",icon:"mdi:robot-vacuum",subview:!1,controllerCardOptions:{iconOn:"mdi:robot-vacuum",iconOff:"mdi:robot-vacuum-off",onService:"vacuum.start",offService:"vacuum.stop"}}),s.set(this,{title:n.H.customLocalize("vacuum.all_vacuums"),subtitle:`${n.H.getCountTemplate(d(i,i,"f",a),"ne","off")} ${n.H.customLocalize("vacuum.vacuums")} `+n.H.customLocalize("generic.busy")}),this.config=Object.assign(this.config,d(this,o,"f"),e),this.viewControllerCard=new c.ControllerCard(this.targetDomain(d(i,i,"f",a)),{...d(this,s,"f"),..."controllerCardOptions"in this.config?this.config.controllerCardOptions:{}}).createCard()}}i=h,o=new WeakMap,s=new WeakMap,a={value:"vacuum"}},560:(e,t,r)=>{var i={"./AbstractCard":[818],"./AbstractCard.ts":[818],"./AreaCard":[301,792],"./AreaCard.ts":[301,792],"./BinarySensorCard":[151,792],"./BinarySensorCard.ts":[151,792],"./CameraCard":[723,792],"./CameraCard.ts":[723,792],"./ClimateCard":[721,792],"./ClimateCard.ts":[721,792],"./ControllerCard":[896],"./ControllerCard.ts":[896],"./CoverCard":[33,792],"./CoverCard.ts":[33,792],"./FanCard":[667,792],"./FanCard.ts":[667,792],"./HaAreaCard":[42,792],"./HaAreaCard.ts":[42,792],"./InputSelectCard":[810,792],"./InputSelectCard.ts":[810,792],"./LightCard":[254,792],"./LightCard.ts":[254,792],"./LockCard":[936,792],"./LockCard.ts":[936,792],"./MediaPlayerCard":[879,792],"./MediaPlayerCard.ts":[879,792],"./MiscellaneousCard":[486,792],"./MiscellaneousCard.ts":[486,792],"./NumberCard":[137,792],"./NumberCard.ts":[137,792],"./PersonCard":[845,792],"./PersonCard.ts":[845,792],"./SceneCard":[858,792],"./SceneCard.ts":[858,792],"./SelectCard":[212,792],"./SelectCard.ts":[212,792],"./SensorCard":[982],"./SensorCard.ts":[982],"./SwitchCard":[708,792],"./SwitchCard.ts":[708,792],"./VacuumCard":[430,792],"./VacuumCard.ts":[430,792]};function a(e){if(!r.o(i,e))return Promise.resolve().then((()=>{var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}));var t=i[e],a=t[0];return Promise.all(t.slice(1).map(r.e)).then((()=>r(a)))}a.keys=()=>Object.keys(i),a.id=560,e.exports=a},216:(e,t,r)=>{var i={"./AbstractChip":[222,792],"./AbstractChip.ts":[222,792],"./ClimateChip":[757,792],"./ClimateChip.ts":[757,792],"./CoverChip":[589,792],"./CoverChip.ts":[589,792],"./FanChip":[867,792],"./FanChip.ts":[867,792],"./LightChip":[741,792],"./LightChip.ts":[741,792],"./SwitchChip":[908,792],"./SwitchChip.ts":[908,792],"./WeatherChip":[778,792],"./WeatherChip.ts":[778,792]};function a(e){if(!r.o(i,e))return Promise.resolve().then((()=>{var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}));var t=i[e],a=t[0];return r.e(t[1]).then((()=>r(a)))}a.keys=()=>Object.keys(i),a.id=216,e.exports=a},555:(e,t,r)=>{var i={"./AbstractView":[122,792],"./AbstractView.ts":[122,792],"./CameraView":[255,792],"./CameraView.ts":[255,792],"./ClimateView":[559,792],"./ClimateView.ts":[559,792],"./CoverView":[547,792],"./CoverView.ts":[547,792],"./FanView":[626,792],"./FanView.ts":[626,792],"./HomeView":[31,792],"./HomeView.ts":[31,792],"./LightView":[304,792],"./LightView.ts":[304,792],"./SceneView":[204,792],"./SceneView.ts":[204,792],"./SwitchView":[464,792],"./SwitchView.ts":[464,792],"./VacuumView":[499,792],"./VacuumView.ts":[499,792]};function a(e){if(!r.o(i,e))return Promise.resolve().then((()=>{var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}));var t=i[e],a=t[0];return r.e(t[1]).then((()=>r(a)))}a.keys=()=>Object.keys(i),a.id=555,e.exports=a}},i={};function a(e){var t=i[e];if(void 0!==t)return t.exports;var o=i[e]={exports:{}};return r[e](o,o.exports,a),o.exports}a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},t=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,a.t=function(r,i){if(1&i&&(r=this(r)),8&i)return r;if("object"==typeof r&&r){if(4&i&&r.__esModule)return r;if(16&i&&"function"==typeof r.then)return r}var o=Object.create(null);a.r(o);var s={};e=e||[null,t({}),t([]),t(t)];for(var n=2&i&&r;"object"==typeof n&&!~e.indexOf(n);n=t(n))Object.getOwnPropertyNames(n).forEach((e=>s[e]=()=>r[e]));return s.default=()=>r,a.d(o,s),o},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.e=()=>Promise.resolve(),a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),a.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{"use strict";var e=a(245),t=a(982),r=a(896),i=a(107);class o extends HTMLTemplateElement{static async generateDashboard(t){await e.H.initialize(t);const r=t.config?.views??[];let i;for(let t of e.H.getExposedViewIds())try{const o=e.H.sanitizeClassName(t+"View");i=await a(555)(`./${o}`);const s=await new i[o](e.H.strategyOptions.views[t]).getView();s.cards?.length&&r.push(s)}catch(r){e.H.logError(`View '${t}' couldn't be loaded!`,r)}for(let t of e.H.areas)t.hidden||r.push({title:t.name,path:t.area_id??t.name,subview:!0,strategy:{type:"custom:mushroom-strategy",options:{area:t}}});return e.H.strategyOptions.extra_views&&r.push(...e.H.strategyOptions.extra_views),{views:r}}static async generateView(o){const s=e.H.getExposedDomainIds(),n=o.view.strategy?.options?.area??{},c=[...n.extra_cards??[]];let l={area_id:[n.area_id]};for(const o of s){if("default"===o)continue;const s=e.H.sanitizeClassName(o+"Card");let d=[];try{d=await a(560)(`./${s}`).then((a=>{let c=[],d=e.H.getDeviceEntities(n,o);if(d=(0,i.j)(d,o),"undisclosed"===n.area_id&&(l={entity_id:d.map((e=>e.entity_id))}),d.length){const i=new r.ControllerCard(l,e.H.strategyOptions.domains[o]).createCard();if("sensor"===o){const r=e.H.getStateEntities(n,"sensor"),a=[];for(const i of d){const o=r.find((e=>e.entity_id===i.entity_id));let s=e.H.strategyOptions.card_options?.[i.entity_id];o?.attributes.unit_of_measurement&&(s={type:"custom:mini-graph-card",entities:[i.entity_id],...s},a.push(new t.SensorCard(i,s).getCard()))}return a.length&&(c.push({type:"vertical-stack",cards:a}),c.unshift(i)),c}for(const t of d){let r,i=e.H.strategyOptions.card_options?.[t.entity_id];t.device_id&&(r=e.H.strategyOptions.card_options?.[t.device_id]),c.push(new a[s](t,i).getCard())}if("binary_sensor"===o){const e=[];for(let t=0;t!s.includes(e.entity_id.split(".",1)[0])));if(t=(0,i.j)(t,"default"),t.length){let i=[];try{i=await Promise.resolve().then(a.bind(a,486)).then((i=>{const a=[new r.ControllerCard(l,e.H.strategyOptions.domains.default).createCard()];for(const r of t){let t=e.H.strategyOptions.card_options?.[r.entity_id];e.H.strategyOptions.card_options?.[r.device_id??"null"],a.push(new i.MiscellaneousCard(r,t).getCard())}return a}))}catch(t){e.H.logError("An error occurred while creating the domain cards!",t)}c.push({type:"vertical-stack",cards:i})}}return{cards:c}}}customElements.define("ll-strategy-mushroom-strategy",o),console.info("%c Mushroom Strategy %c ".concat("v2.2.0"," "),"color: white; background: coral; font-weight: 700;","color: coral; background: white; font-weight: 700;")})()})(); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 60ebc8c..3abdb49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mushroom-strategy", - "version": "2.2.0", + "version": "2.2.1", "lockfileVersion": 3, "requires": true, "packages": { @@ -14,7 +14,7 @@ "devDependencies": { "home-assistant-js-websocket": "^9", "superstruct": "^1", - "ts-loader": "^9", + "ts-loader": "^9.5.0", "ts-node": "^10", "typescript": "^5", "version-bump-prompt": "^6", diff --git a/package.json b/package.json index 3041f31..f766e7b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mushroom-strategy", - "version": "2.2.0", + "version": "2.2.1", "description": "Automatically create a dashboard using Mushroom cards", "keywords": [ "strategy", diff --git a/src/Helper.ts b/src/Helper.ts index 00f4db4..1b109e8 100644 --- a/src/Helper.ts +++ b/src/Helper.ts @@ -8,6 +8,14 @@ import {generic} from "./types/strategy/generic"; import setupCustomLocalize from "./localize"; import {applyEntityCategoryFilters} from "./utillties/filters"; import StrategyArea = generic.StrategyArea; +import ViewConfig = generic.StrategyViewConfig; +import SupportedDomains = generic.SupportedDomains; +import supportedViews = generic.SupportedViews; +import isSortable = generic.isSortable; +import AllDomainsConfig = generic.AllDomainsConfig; +import SingleDomainConfig = generic.SingleDomainConfig; +import isSupportedView = generic.isSupportedView; +import isSupportedDomain = generic.isSupportedDomain; /** * Helper Class @@ -40,7 +48,7 @@ class Helper { static #areas: StrategyArea[] = []; /** - * An array of state entities from Home Assistant's Hass object. + * An array of state entities from Home Assistant's Hass-object. * * @type {HassEntities} * @private @@ -137,11 +145,11 @@ class Helper { /** * Initialize this module. * - * @param {generic.DashBoardInfo} info Strategy information object. + * @param {generic.DashboardInfo} info Strategy information object. * @returns {Promise} * @static */ - static async initialize(info: generic.DashBoardInfo): Promise { + static async initialize(info: generic.DashboardInfo): Promise { // Initialize properties. this.customLocalize = setupCustomLocalize(info.hass); @@ -191,16 +199,27 @@ class Helper { // Sort custom and default views of the strategy options by order first and then by title. this.#strategyOptions.views = Object.fromEntries( Object.entries(this.#strategyOptions.views).sort(([, a], [, b]) => { - return (a.order ?? Infinity) - (b.order ?? Infinity) || (a.title ?? "undefined").localeCompare(b.title ?? "undefined"); + const viewA = a as ViewConfig; + const viewB = b as ViewConfig; + + return (viewA.order ?? Infinity) - (viewB.order ?? Infinity) + || (viewA.title ?? "undefined").localeCompare(viewB.title ?? "undefined"); }), - ); + ) as Record; // Sort custom and default domains of the strategy options by order first and then by title. this.#strategyOptions.domains = Object.fromEntries( Object.entries(this.#strategyOptions.domains).sort(([, a], [, b]) => { - return (a.order ?? Infinity) - (b.order ?? Infinity) || (a.title ?? "undefined").localeCompare(b.title ?? "undefined"); + if (isSortable(a) && isSortable(b)) { + const orderA = ('order' in a) ? a.order ?? Infinity : Infinity; + const orderB = ('order' in b) ? b.order ?? Infinity : Infinity; + + return orderA - orderB || (a.title ?? "undefined").localeCompare(b.title ?? "undefined"); + } + + return 0; }), - ); + ) as { [K in SupportedDomains]: K extends "_" ? AllDomainsConfig : SingleDomainConfig; }; this.#initialized = true; } @@ -228,7 +247,8 @@ class Helper { * @return {string} The template string. * @static */ - static getCountTemplate(domain: string, operator: string, value: string): string { + static getCountTemplate(domain: SupportedDomains, operator: string, value: string): string { + // noinspection JSMismatchedCollectionQueryUpdate /** * Array of entity state-entries, filtered by domain. * @@ -390,27 +410,31 @@ class Helper { /** * Get the ids of the views which aren't set to hidden in the strategy options. * - * @return {string[]} An array of view ids. + * @return {SupportedViews[]} An array of view ids. */ - static getExposedViewIds(): string[] { + static getExposedViewIds(): supportedViews[] { if (!this.isInitialized()) { console.warn("Helper class should be initialized before calling this method!"); } - return this.#getObjectKeysByPropertyValue(this.#strategyOptions.views, "hidden", false); + const ids = this.#getObjectKeysByPropertyValue(this.#strategyOptions.views, "hidden", false); + + return ids.filter(isSupportedView); } /** * Get the ids of the domain ids which aren't set to hidden in the strategy options. * - * @return {string[]} An array of domain ids. + * @return {SupportedDomains[]} An array of domain ids. */ - static getExposedDomainIds(): string[] { + static getExposedDomainIds(): SupportedDomains[] { if (!this.isInitialized()) { console.warn("Helper class should be initialized before calling this method!"); } - return this.#getObjectKeysByPropertyValue(this.#strategyOptions.domains, "hidden", false); + const ids = this.#getObjectKeysByPropertyValue(this.#strategyOptions.domains, "hidden", false); + + return ids.filter(isSupportedDomain); } /** diff --git a/src/cards/CameraCard.ts b/src/cards/CameraCard.ts index 499007e..812ec81 100644 --- a/src/cards/CameraCard.ts +++ b/src/cards/CameraCard.ts @@ -1,7 +1,7 @@ import {AbstractCard} from "./AbstractCard"; import {cards} from "../types/strategy/cards"; import {EntityRegistryEntry} from "../types/homeassistant/data/entity_registry"; -import {PictureEntityCardConfig} from "../types/homeassistant/panels/lovelave/cards/types"; +import {PictureEntityCardConfig} from "../types/homeassistant/panels/lovelace/cards/types"; // noinspection JSUnusedGlobalSymbols Class is dynamically imported. /** diff --git a/src/cards/ControllerCard.ts b/src/cards/ControllerCard.ts index 273d96f..b33878c 100644 --- a/src/cards/ControllerCard.ts +++ b/src/cards/ControllerCard.ts @@ -1,7 +1,7 @@ import {cards} from "../types/strategy/cards"; -import {StackCardConfig} from "../types/homeassistant/lovelace/cards/types"; import {LovelaceCardConfig} from "../types/homeassistant/data/lovelace"; import {HassServiceTarget} from "home-assistant-js-websocket"; +import {StackCardConfig} from "../types/homeassistant/panels/lovelace/cards/types"; /** * Controller Card class. diff --git a/src/cards/HaAreaCard.ts b/src/cards/HaAreaCard.ts index 8652f44..ce31b34 100644 --- a/src/cards/HaAreaCard.ts +++ b/src/cards/HaAreaCard.ts @@ -1,7 +1,7 @@ import {AbstractCard} from "./AbstractCard"; import {cards} from "../types/strategy/cards"; import {AreaRegistryEntry} from "../types/homeassistant/data/area_registry"; -import {AreaCardConfig} from "../types/homeassistant/lovelace/cards/types"; +import {AreaCardConfig} from "../types/homeassistant/panels/lovelace/cards/types"; // noinspection JSUnusedGlobalSymbols Class is dynamically imported. /** diff --git a/src/configurationDefaults.ts b/src/configurationDefaults.ts index 961f3ed..2d8ab4b 100644 --- a/src/configurationDefaults.ts +++ b/src/configurationDefaults.ts @@ -8,27 +8,72 @@ export const getConfigurationDefaults = (localize: Function): StrategyDefaults = return { areas: { undisclosed: { + aliases: [], area_id: "undisclosed", + created_at: 0, floor_id: null, - name: "Undisclosed", - picture: null, + hidden: false, + humidity_entity_id: null, icon: "mdi:floor-plan", labels: [], - aliases: [], - hidden: false, + modified_at: 0, + name: "Undisclosed", + picture: null, + temperature_entity_id: null, } }, + card_options: {}, + chips: {}, debug: false, domains: { _: { hide_config_entities: true, hide_diagnostic_entities: true, }, + binary_sensor: { + title: `${localize("sensor.binary")} ` + localize("sensor.sensors"), + showControls: false, + hidden: false, + }, + camera: { + title: localize("camera.cameras"), + showControls: false, + hidden: false, + }, + climate: { + title: localize("climate.climates"), + showControls: false, + hidden: false, + }, + cover: { + title: localize("cover.covers"), + showControls: true, + iconOn: "mdi:arrow-up", + iconOff: "mdi:arrow-down", + onService: "cover.open_cover", + offService: "cover.close_cover", + hidden: false, + }, default: { title: localize("generic.miscellaneous"), showControls: false, hidden: false, }, + fan: { + title: localize("fan.fans"), + showControls: true, + iconOn: "mdi:fan", + iconOff: "mdi:fan-off", + onService: "fan.turn_on", + offService: "fan.turn_off", + hidden: false, + }, + + input_select: { + title: localize("input_select.input_selects"), + showControls: false, + hidden: false, + }, light: { title: localize("light.lights"), showControls: true, @@ -38,28 +83,35 @@ export const getConfigurationDefaults = (localize: Function): StrategyDefaults = offService: "light.turn_off", hidden: false, }, + lock: { + title: localize("lock.locks"), + showControls: false, + hidden: false, + }, + media_player: { + title: localize("media_player.media_players"), + showControls: false, + hidden: false, + }, + number: { + title: localize("generic.numbers"), + showControls: false, + hidden: false, + }, scene: { title: localize("scene.scenes"), showControls: false, onService: "scene.turn_on", hidden: false, }, - fan: { - title: localize("fan.fans"), - showControls: true, - iconOn: "mdi:fan", - iconOff: "mdi:fan-off", - onService: "fan.turn_on", - offService: "fan.turn_off", + select: { + title: localize("select.selects"), + showControls: false, hidden: false, }, - cover: { - title: localize("cover.covers"), - showControls: true, - iconOn: "mdi:arrow-up", - iconOff: "mdi:arrow-down", - onService: "cover.open_cover", - offService: "cover.close_cover", + sensor: { + title: localize("sensor.sensors"), + showControls: false, hidden: false, }, switch: { @@ -71,61 +123,34 @@ export const getConfigurationDefaults = (localize: Function): StrategyDefaults = offService: "switch.turn_off", hidden: false, }, - camera: { - title: localize("camera.cameras"), - showControls: false, - hidden: false, - }, - lock: { - title: localize("lock.locks"), - showControls: false, - hidden: false, - }, - climate: { - title: localize("climate.climates"), - showControls: false, - hidden: false, - }, - media_player: { - title: localize("media_player.media_players"), - showControls: false, - hidden: false, - }, - sensor: { - title: localize("sensor.sensors"), - showControls: false, - hidden: false, - }, - binary_sensor: { - title: `${localize("sensor.binary")} ` + localize("sensor.sensors"), - showControls: false, - hidden: false, - }, - number: { - title: localize("generic.numbers"), - showControls: false, - hidden: false, - }, vacuum: { title: localize("vacuum.vacuums"), showControls: true, hidden: false, }, - select: { - title: localize("select.selects"), - showControls: false, - hidden: false, - }, - input_select: { - title: localize("input_select.input_selects"), - showControls: false, - hidden: false, - }, }, + extra_cards: [], + extra_views: [], home_view: { hidden: [], }, views: { + camera: { + order: 7, + hidden: false, + }, + climate: { + order: 6, + hidden: false, + }, + cover: { + order: 4, + hidden: false, + }, + fan: { + order: 3, + hidden: false, + }, home: { order: 1, hidden: false, @@ -134,34 +159,19 @@ export const getConfigurationDefaults = (localize: Function): StrategyDefaults = order: 2, hidden: false, }, - fan: { - order: 3, - hidden: false, - }, - cover: { - order: 4, + scene: { + order: 9, hidden: false, }, switch: { order: 5, hidden: false, }, - climate: { - order: 6, - hidden: false, - }, - camera: { - order: 7, - hidden: false, - }, vacuum: { order: 8, hidden: false, }, - scene: { - order: 9, - hidden: false, - }, - } + }, + quick_access_cards: [] }; }; diff --git a/src/mushroom-strategy.ts b/src/mushroom-strategy.ts index 0880fc7..49f49c5 100644 --- a/src/mushroom-strategy.ts +++ b/src/mushroom-strategy.ts @@ -1,13 +1,18 @@ import {Helper} from "./Helper"; import {SensorCard} from "./cards/SensorCard"; import {ControllerCard} from "./cards/ControllerCard"; -import {generic} from "./types/strategy/generic"; -import {LovelaceCardConfig, LovelaceConfig, LovelaceViewConfig} from "./types/homeassistant/data/lovelace"; -import {StackCardConfig} from "./types/homeassistant/lovelace/cards/types"; import {EntityCardConfig} from "./types/lovelace-mushroom/cards/entity-card-config"; import {HassServiceTarget} from "home-assistant-js-websocket"; import {applyEntityCategoryFilters} from "./utillties/filters"; +import {LovelaceConfig} from "./types/homeassistant/data/lovelace/config/types"; +import {LovelaceViewConfig, LovelaceViewRawConfig} from "./types/homeassistant/data/lovelace/config/view"; +import {LovelaceCardConfig} from "./types/homeassistant/data/lovelace"; +import {StackCardConfig} from "./types/homeassistant/panels/lovelace/cards/types"; +import {generic} from "./types/strategy/generic"; +import {views} from "./types/strategy/views"; +import ViewConfig = views.ViewConfig; import StrategyArea = generic.StrategyArea; +import SupportedDomains = generic.SupportedDomains; /** * Mushroom Dashboard Strategy.
@@ -28,14 +33,14 @@ class MushroomStrategy extends HTMLTemplateElement { * * Called when opening a dashboard. * - * @param {generic.DashBoardInfo} info Dashboard strategy information object. + * @param {generic.DashboardInfo} info Dashboard strategy information object. * @return {Promise} */ - static async generateDashboard(info: generic.DashBoardInfo): Promise { + static async generateDashboard(info: generic.DashboardInfo): Promise { await Helper.initialize(info); // Create views. - const views: LovelaceViewConfig[] = info.config?.views ?? []; + const views: LovelaceViewRawConfig[] = []; let viewModule; @@ -44,7 +49,7 @@ class MushroomStrategy extends HTMLTemplateElement { try { const viewType = Helper.sanitizeClassName(viewId + "View"); viewModule = await import(`./views/${viewType}`); - const view: LovelaceViewConfig = await new viewModule[viewType](Helper.strategyOptions.views[viewId]).getView(); + const view: ViewConfig = await new viewModule[viewType](Helper.strategyOptions.views[viewId]).getView(); if (view.cards?.length) { views.push(view); @@ -216,7 +221,7 @@ class MushroomStrategy extends HTMLTemplateElement { // Create cards for any other domain. // Collect entities of the current area and unexposed domains. let miscellaneousEntities = Helper.getDeviceEntities(area).filter( - entity => !exposedDomainIds.includes(entity.entity_id.split(".", 1)[0]) + entity => !exposedDomainIds.includes(entity.entity_id.split(".", 1)[0] as SupportedDomains) ); // Exclude hidden Config and Diagnostic entities. @@ -234,7 +239,6 @@ class MushroomStrategy extends HTMLTemplateElement { for (const entity of miscellaneousEntities) { let cardOptions = Helper.strategyOptions.card_options?.[entity.entity_id]; - let deviceOptions = Helper.strategyOptions.card_options?.[entity.device_id ?? "null"]; miscellaneousCards.push(new cardModule.MiscellaneousCard(entity, cardOptions).getCard()); } @@ -261,7 +265,7 @@ class MushroomStrategy extends HTMLTemplateElement { customElements.define("ll-strategy-mushroom-strategy", MushroomStrategy); -const version = "v2.2.0"; +const version = "v2.2.1"; console.info( "%c Mushroom Strategy %c ".concat(version, " "), "color: white; background: coral; font-weight: 700;", "color: coral; background: white; font-weight: 700;" diff --git a/src/types/homeassistant/common/translations/localize.ts b/src/types/homeassistant/common/translations/localize.ts new file mode 100644 index 0000000..96a2a1d --- /dev/null +++ b/src/types/homeassistant/common/translations/localize.ts @@ -0,0 +1,49 @@ +import type {TranslationDict} from "../../types"; + +// Exclude some patterns from key type checking for now +// These are intended to be removed as errors are fixed +// Fixing component category will require tighter definition of types from backend and/or web socket +export type LocalizeKeys = + | FlattenObjectKeys> + | `panel.${string}` + | `ui.card.alarm_control_panel.${string}` + | `ui.card.weather.attributes.${string}` + | `ui.card.weather.cardinal_direction.${string}` + | `ui.card.lawn_mower.actions.${string}` + | `ui.components.calendar.event.rrule.${string}` + | `ui.components.selectors.file.${string}` + | `ui.components.logbook.messages.detected_device_classes.${string}` + | `ui.components.logbook.messages.cleared_device_classes.${string}` + | `ui.dialogs.entity_registry.editor.${string}` + | `ui.dialogs.more_info_control.lawn_mower.${string}` + | `ui.dialogs.more_info_control.vacuum.${string}` + | `ui.dialogs.quick-bar.commands.${string}` + | `ui.dialogs.unhealthy.reason.${string}` + | `ui.dialogs.unsupported.reason.${string}` + | `ui.panel.config.${string}.${"caption" | "description"}` + | `ui.panel.config.dashboard.${string}` + | `ui.panel.config.zha.${string}` + | `ui.panel.config.zwave_js.${string}` + | `ui.panel.lovelace.card.${string}` + | `ui.panel.lovelace.editor.${string}` + | `ui.panel.page-authorize.form.${string}` + | `component.${string}`; + +// Tweaked from https://www.raygesualdo.com/posts/flattening-object-keys-with-typescript-types +export type FlattenObjectKeys< + T extends Record, + Key extends keyof T = keyof T, +> = Key extends string + ? T[Key] extends Record + ? `${Key}.${FlattenObjectKeys}` + : `${Key}` + : never; + +// Later, don't return string when HTML is passed, and don't allow undefined +export type LocalizeFunc = ( + key: Keys, + values?: Record< + string, + string | number | {_$litType$: 1, strings: TemplateStringsArray, values: Array} | null | undefined + > +) => string; diff --git a/src/types/homeassistant/data/area_registry.ts b/src/types/homeassistant/data/area_registry.ts index 0d07e9a..211dc3b 100644 --- a/src/types/homeassistant/data/area_registry.ts +++ b/src/types/homeassistant/data/area_registry.ts @@ -1,20 +1,26 @@ +import {RegistryEntry} from "./registry"; + /** - * Area Entity. + * Entry in the Area Registry. * + * @property {string[]} aliases Array of aliases of the area. * @property {string} area_id The id of the area. * @property {string|null} floor_id The id of the area's floor. - * @property {string} name Name of the area. - * @property {string|null} picture URL to a picture that should be used instead of showing the domain icon. + * @property {string|null} humidity_entity_id The id of the area's humidity sensor. * @property {string|null} icon Icon to show. * @property {string[]} labels Labels allow grouping elements irrespective of their physical location or type. - * @property {string[]} aliases Array of aliases of the area. + * @property {string} name Name of the area. + * @property {string|null} picture URL to a picture that should be used instead of showing the domain icon. + * @property {string|null} temperature_entity_id The id of the area's temperature sensor. */ -export interface AreaRegistryEntry { +export interface AreaRegistryEntry extends RegistryEntry { + aliases: string[]; area_id: string; floor_id: string | null; - name: string; - picture: string | null; + humidity_entity_id: string | null; icon: string | null; labels: string[]; - aliases: string[]; + name: string; + picture: string | null; + temperature_entity_id: string | null; } diff --git a/src/types/homeassistant/data/climate.ts b/src/types/homeassistant/data/climate.ts index 468350a..e24784d 100644 --- a/src/types/homeassistant/data/climate.ts +++ b/src/types/homeassistant/data/climate.ts @@ -9,11 +9,3 @@ export const HVAC_MODES = [ ] as const; export type HvacMode = (typeof HVAC_MODES)[number]; - -HVAC_MODES.reduce( - (order, mode, index) => { - order[mode] = index; - return order; - }, - {} as Record -); diff --git a/src/types/homeassistant/data/device_registry.ts b/src/types/homeassistant/data/device_registry.ts index 6b0623e..61c510a 100644 --- a/src/types/homeassistant/data/device_registry.ts +++ b/src/types/homeassistant/data/device_registry.ts @@ -2,30 +2,51 @@ * Device Entity. * * @property {string} id Unique ID of a device (generated by Home Assistant). - * @property {string[]} config_entries - * @property {Array} connections - * @property {Array} identifiers - * @property {string | null} manufacturer - * @property {string | null} model - * @property {string | null} name - * @property {string | null} sw_version - * @property {string | null} hw_version - * @property {string | null} serial_number - * @property {string | null} via_device_id + * @property {string[]} config_entries Config entries that are linked to this device. + * @property {Record} config_entries_subentries + * @property {[string, string][]} connections A set of tuples of (connection_type, connection identifier). + * Connection types are defined in the device registry module. + * Each item in the set uniquely defines a device entry, meaning another + * device can't have the same connection. + * @property {[string, string][]} identifiers Set of (DOMAIN, identifier) tuples. + * Identifiers identify the device in the outside world. + * An example is a serial number. + * Each item in the set uniquely defines a device entry, meaning another + * device can't have the same identifier. + * @property {string | null} manufacturer The manufacturer of the device. + * @property {string | null} model The model name of the device. + * @property {string | null} model_id The model identifier of the device. + * @property {string | null} name Name of this device + * @property {string[]} labels + * @property {string | null} sw_version The firmware version of the device. + * @property {string | null} hw_version The hardware version of the device. + * @property {string | null} serial_number The serial number of the device. + * Unlike a serial number in the identifiers set, this does not need to be + * unique. + * @property {string | null} via_device_id Identifier of a device that routes messages between this device and Home Assistant. + * Examples of such devices are hubs, or parent devices of a sub-device. + * This is used to show device topology in Home Assistant. * @property {string} area_id The Area which the device is placed in. - * @property {string | null} name_by_user - * @property {string[] | null} entry_type + * @property {string | null} name_by_user The user configured name of the device. + * @property {string[] | null} entry_type The type of entry. Possible values are None and DeviceEntryType enum members + * (only service). * @property {string | null} disabled_by Indicates by what this entity is disabled. - * @property {string | null} configuration_url + * @property {string | null} configuration_url A URL on which the device or service can be configured, + * linking to paths inside the Home Assistant UI can be done by using + * homeassistant://. + * @property {string | null} primary_config_entry */ export interface DeviceRegistryEntry { id: string; config_entries: string[]; - connections: Array<[string, string]>; - identifiers: Array<[string, string]>; + config_entries_subentries: Record; + connections: [string, string][]; + identifiers: [string, string][]; manufacturer: string | null; model: string | null; + model_id: string | null; name: string | null; + labels: string[]; sw_version: string | null; hw_version: string | null; serial_number: string | null; @@ -35,4 +56,5 @@ export interface DeviceRegistryEntry { entry_type: "service" | null; disabled_by: "user" | "integration" | "config_entry" | null; configuration_url: string | null; + primary_config_entry: string | null; } diff --git a/src/types/homeassistant/data/entity_registry.ts b/src/types/homeassistant/data/entity_registry.ts index f538a27..f856933 100644 --- a/src/types/homeassistant/data/entity_registry.ts +++ b/src/types/homeassistant/data/entity_registry.ts @@ -5,13 +5,16 @@ type EntityCategory = "config" | "diagnostic"; export interface EntityRegistryDisplayEntry { entity_id: string; name?: string; + icon?: string; device_id?: string; area_id?: string; + labels: string[]; hidden?: boolean; entity_category?: EntityCategory; translation_key?: string; platform?: string; display_precision?: number; + has_entity_name?: boolean; } /** @@ -23,8 +26,10 @@ export interface EntityRegistryDisplayEntry { * @property {string | null} icon * @property {string | null} platform * @property {string | null} config_entry_id + * @property {string | null} config_subentry_id * @property {string | null} device_id The id of the device to which this entity is linked. * @property {string | null} area_id The id of the area to which this entity is linked. + * @property {string[]} labels * @property {string | null} disabled_by Indicates by what this entity is disabled. * @property {Object} hidden_by Indicates by what this entity is hidden. * @property {EntityCategory | null} entity_category @@ -33,6 +38,7 @@ export interface EntityRegistryDisplayEntry { * @property {string} unique_id * @property {string} [translation_key] * @property {EntityRegistryOptions | null} options + * @property {Record} categories */ export interface EntityRegistryEntry { id: string; @@ -41,8 +47,10 @@ export interface EntityRegistryEntry { icon: string | null; platform: string; config_entry_id: string | null; + config_subentry_id: string | null; device_id: string | null; area_id: string | null; + labels: string[]; disabled_by: "user" | "device" | "integration" | "config_entry" | null; hidden_by: Exclude; entity_category: EntityCategory | null; @@ -51,6 +59,7 @@ export interface EntityRegistryEntry { unique_id: string; translation_key?: string; options: EntityRegistryOptions | null; + categories: Record; } export interface SensorEntityOptions { @@ -71,6 +80,10 @@ export interface LockEntityOptions { default_code?: string | null; } +export interface AlarmControlPanelEntityOptions { + default_code?: string | null; +} + export interface WeatherEntityOptions { precipitation_unit?: string | null; pressure_unit?: string | null; @@ -81,11 +94,13 @@ export interface WeatherEntityOptions { export interface SwitchAsXEntityOptions { entity_id: string; + invert: boolean; } export interface EntityRegistryOptions { number?: NumberEntityOptions; sensor?: SensorEntityOptions; + alarm_control_panel?: AlarmControlPanelEntityOptions; lock?: LockEntityOptions; weather?: WeatherEntityOptions; light?: LightEntityOptions; diff --git a/src/types/homeassistant/data/floor_registry.ts b/src/types/homeassistant/data/floor_registry.ts new file mode 100644 index 0000000..eccbaa5 --- /dev/null +++ b/src/types/homeassistant/data/floor_registry.ts @@ -0,0 +1,9 @@ +import {RegistryEntry} from "./registry"; + +export interface FloorRegistryEntry extends RegistryEntry { + floor_id: string; + name: string; + level: number | null; + icon: string | null; + aliases: string[]; +} diff --git a/src/types/homeassistant/data/frontend.ts b/src/types/homeassistant/data/frontend.ts new file mode 100644 index 0000000..3155312 --- /dev/null +++ b/src/types/homeassistant/data/frontend.ts @@ -0,0 +1,3 @@ +export interface CoreFrontendUserData { + showAdvanced?: boolean; +} diff --git a/src/types/homeassistant/data/lovelace.ts b/src/types/homeassistant/data/lovelace.ts index e893952..6eb3c69 100644 --- a/src/types/homeassistant/data/lovelace.ts +++ b/src/types/homeassistant/data/lovelace.ts @@ -1,54 +1,17 @@ import {HassServiceTarget} from "home-assistant-js-websocket"; - -export type LovelaceStrategyConfig = { - type: string; - [key: string]: any; -}; - -export interface LovelaceConfig { - title?: string; - strategy?: LovelaceStrategyConfig; - views: LovelaceViewConfig[]; - background?: string; -} - -/** - * View Config. - * - * @see https://www.home-assistant.io/dashboards/views/ - */ -export interface LovelaceViewConfig { - index?: number; - title?: string; - type?: string; - strategy?: LovelaceStrategyConfig; - badges?: Array; - cards?: LovelaceCardConfig[]; - path?: string; - icon?: string; - theme?: string; - panel?: boolean; - background?: string; - visible?: boolean | ShowViewConfig[]; - subview?: boolean; - back_path?: string; -} - -export interface ShowViewConfig { - user?: string; -} - -export interface LovelaceBadgeConfig { - type?: string; - [key: string]: any; -} +import {LovelaceGridOptions, LovelaceLayoutOptions} from "../panels/lovelace/types"; +import {Condition} from "../panels/common/validate-condition"; export interface LovelaceCardConfig { index?: number; view_index?: number; view_layout?: any; + /** @deprecated Use `grid_options` instead */ + layout_options?: LovelaceLayoutOptions; + grid_options?: LovelaceGridOptions; type: string; [key: string]: any; + visibility?: Condition[]; } export interface ToggleActionConfig extends BaseActionConfig { diff --git a/src/types/homeassistant/data/lovelace/config/badge.ts b/src/types/homeassistant/data/lovelace/config/badge.ts new file mode 100644 index 0000000..d9e2450 --- /dev/null +++ b/src/types/homeassistant/data/lovelace/config/badge.ts @@ -0,0 +1,7 @@ +import {Condition} from "../../../panels/common/validate-condition"; + +export interface LovelaceBadgeConfig { + type: string; + [key: string]: any; + visibility?: Condition[]; +} diff --git a/src/types/homeassistant/data/lovelace/config/card.ts b/src/types/homeassistant/data/lovelace/config/card.ts new file mode 100644 index 0000000..b25ce02 --- /dev/null +++ b/src/types/homeassistant/data/lovelace/config/card.ts @@ -0,0 +1,14 @@ +import {Condition} from "../../../panels/common/validate-condition"; +import {LovelaceGridOptions, LovelaceLayoutOptions} from "../../../panels/lovelace/types"; + +export interface LovelaceCardConfig { + index?: number; + view_index?: number; + view_layout?: any; + /** @deprecated Use `grid_options` instead */ + layout_options?: LovelaceLayoutOptions; + grid_options?: LovelaceGridOptions; + type: string; + [key: string]: any; + visibility?: Condition[]; +} diff --git a/src/types/homeassistant/data/lovelace/config/section.ts b/src/types/homeassistant/data/lovelace/config/section.ts new file mode 100644 index 0000000..3b5b07e --- /dev/null +++ b/src/types/homeassistant/data/lovelace/config/section.ts @@ -0,0 +1,27 @@ +import {LovelaceStrategyConfig} from "./strategy"; +import {LovelaceCardConfig} from "../../lovelace"; +import {Condition} from "../../../panels/common/validate-condition"; + +export interface LovelaceBaseSectionConfig { + visibility?: Condition[]; + column_span?: number; + row_span?: number; + /** + * @deprecated Use heading card instead. + */ + title?: string; +} + +export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig { + type?: string; + cards?: LovelaceCardConfig[]; +} + +export interface LovelaceStrategySectionConfig + extends LovelaceBaseSectionConfig { + strategy: LovelaceStrategyConfig; +} + +export type LovelaceSectionRawConfig = + | LovelaceSectionConfig + | LovelaceStrategySectionConfig; diff --git a/src/types/homeassistant/data/lovelace/config/strategy.ts b/src/types/homeassistant/data/lovelace/config/strategy.ts new file mode 100644 index 0000000..5c35de4 --- /dev/null +++ b/src/types/homeassistant/data/lovelace/config/strategy.ts @@ -0,0 +1,4 @@ +export interface LovelaceStrategyConfig { + type: string; + [key: string]: any; +} diff --git a/src/types/homeassistant/data/lovelace/config/types.ts b/src/types/homeassistant/data/lovelace/config/types.ts new file mode 100644 index 0000000..d1b8b53 --- /dev/null +++ b/src/types/homeassistant/data/lovelace/config/types.ts @@ -0,0 +1,10 @@ +import {LovelaceViewRawConfig} from "./view"; + +export interface LovelaceDashboardBaseConfig {} + +export interface LovelaceConfig extends LovelaceDashboardBaseConfig { + background?: string; + views: LovelaceViewRawConfig[]; +} + + diff --git a/src/types/homeassistant/data/lovelace/config/view.ts b/src/types/homeassistant/data/lovelace/config/view.ts new file mode 100644 index 0000000..3173211 --- /dev/null +++ b/src/types/homeassistant/data/lovelace/config/view.ts @@ -0,0 +1,74 @@ +import {LovelaceStrategyConfig} from "./strategy"; +import {LovelaceSectionRawConfig} from "./section"; +import {LovelaceCardConfig} from "./card"; +import {LovelaceBadgeConfig} from "./badge"; + +export interface ShowViewConfig { + user?: string; +} + +export interface LovelaceViewBackgroundConfig { + image?: string; + opacity?: number; + size?: "auto" | "cover" | "contain"; + alignment?: + | "top left" + | "top center" + | "top right" + | "center left" + | "center" + | "center right" + | "bottom left" + | "bottom center" + | "bottom right"; + repeat?: "repeat" | "no-repeat"; + attachment?: "scroll" | "fixed"; +} + +export interface LovelaceViewHeaderConfig { + card?: LovelaceCardConfig; + layout?: "start" | "center" | "responsive"; + badges_position?: "bottom" | "top"; +} + +export interface LovelaceBaseViewConfig { + index?: number; + title?: string; + path?: string; + icon?: string; + theme?: string; + panel?: boolean; + background?: string | LovelaceViewBackgroundConfig; + visible?: boolean | ShowViewConfig[]; + subview?: boolean; + back_path?: string; + // Only used for section view, it should move to a section view config type when the views will have a dedicated editor. + max_columns?: number; + dense_section_placement?: boolean; + top_margin?: boolean; +} + +/** + * View Config. + * + * @see https://www.home-assistant.io/dashboards/views/ + */ +export interface LovelaceViewConfig extends LovelaceBaseViewConfig { + type?: string; + badges?: (string | Partial)[]; // Badge can be just an entity_id or without type + cards?: LovelaceCardConfig[]; + sections?: LovelaceSectionRawConfig[]; + header?: LovelaceViewHeaderConfig; +} + +export interface LovelaceStrategyViewConfig extends LovelaceBaseViewConfig { + strategy: LovelaceStrategyConfig; +} + +export interface LovelaceStrategyViewConfig extends LovelaceBaseViewConfig { + strategy: LovelaceStrategyConfig; +} + +export type LovelaceViewRawConfig = + | LovelaceViewConfig + | LovelaceStrategyViewConfig; diff --git a/src/types/homeassistant/data/registry.ts b/src/types/homeassistant/data/registry.ts new file mode 100644 index 0000000..985d66a --- /dev/null +++ b/src/types/homeassistant/data/registry.ts @@ -0,0 +1,4 @@ +export interface RegistryEntry { + created_at: number; + modified_at: number; +} diff --git a/src/types/homeassistant/data/translations.ts b/src/types/homeassistant/data/translations.ts new file mode 100644 index 0000000..45408b0 --- /dev/null +++ b/src/types/homeassistant/data/translations.ts @@ -0,0 +1,87 @@ +// noinspection JSUnusedGlobalSymbols + +import {HomeAssistant} from "../types"; + +export enum NumberFormat { + language = "language", + system = "system", + comma_decimal = "comma_decimal", + decimal_comma = "decimal_comma", + space_comma = "space_comma", + none = "none", +} + +export enum TimeFormat { + language = "language", + system = "system", + am_pm = "12", + twenty_four = "24", +} + +export enum TimeZone { + local = "local", + server = "server", +} + +export enum DateFormat { + language = "language", + system = "system", + DMY = "DMY", + MDY = "MDY", + YMD = "YMD", +} + +export enum FirstWeekday { + language = "language", + monday = "monday", + tuesday = "tuesday", + wednesday = "wednesday", + thursday = "thursday", + friday = "friday", + saturday = "saturday", + sunday = "sunday", +} + +export interface FrontendLocaleData { + language: string; + number_format: NumberFormat; + time_format: TimeFormat; + date_format: DateFormat; + first_weekday: FirstWeekday; + time_zone: TimeZone; +} + +export type TranslationCategory = + | "title" + | "state" + | "entity" + | "entity_component" + | "exceptions" + | "config" + | "config_subentries" + | "config_panel" + | "options" + | "device_automation" + | "mfa_setup" + | "system_health" + | "application_credentials" + | "issues" + | "selector" + | "services"; + +export const getHassTranslations = async ( + hass: HomeAssistant, + language: string, + category: TranslationCategory, + integration?: string | string[], + config_flow?: boolean +): Promise> => { + const result = await hass.callWS<{ resources: Record }>({ + type: "frontend/get_translations", + language, + category, + integration, + config_flow, + }); + return result.resources; +}; diff --git a/src/types/homeassistant/data/ws-themes.ts b/src/types/homeassistant/data/ws-themes.ts new file mode 100644 index 0000000..79ff1d2 --- /dev/null +++ b/src/types/homeassistant/data/ws-themes.ts @@ -0,0 +1,26 @@ +export interface ThemeVars { + // Incomplete + "primary-color": string; + "text-primary-color": string; + "accent-color": string; + [key: string]: string; +} + +export type Theme = ThemeVars & { + modes?: { + light?: ThemeVars; + dark?: ThemeVars; + }; +}; + +export interface Themes { + default_theme: string; + default_dark_theme: string | null; + themes: Record; + // Currently effective dark mode. Will never be undefined. If user selected "auto" + // in theme picker, this property will still contain either true or false based on + // what has been determined via system preferences and support for the selected theme. + darkMode: boolean; + // Currently globally active theme name + theme: string; +} diff --git a/src/types/homeassistant/lovelace/cards/types.ts b/src/types/homeassistant/lovelace/cards/types.ts deleted file mode 100644 index 279841d..0000000 --- a/src/types/homeassistant/lovelace/cards/types.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {LovelaceCardConfig} from "../../data/lovelace"; - -/** - * Home Assistant Stack Card Config. - * - * @property {string} type The stack type. - * @property {Object[]} cards The content of the stack. - * - * @see https://www.home-assistant.io/dashboards/horizontal-stack/ - * @see https://www.home-assistant.io/dashboards/vertical-stack/ - */ -export interface StackCardConfig extends LovelaceCardConfig { - cards: LovelaceCardConfig[]; - title?: string; -} - -/** - * Home Assistant Area Card Config. - * - * @see https://www.home-assistant.io/dashboards/area/ - */ -export interface AreaCardConfig extends LovelaceCardConfig { - area: string; - navigation_path?: string; - show_camera?: boolean; -} diff --git a/src/types/homeassistant/panels/common/validate-condition.ts b/src/types/homeassistant/panels/common/validate-condition.ts new file mode 100644 index 0000000..138abc8 --- /dev/null +++ b/src/types/homeassistant/panels/common/validate-condition.ts @@ -0,0 +1,45 @@ +export type Condition = + | NumericStateCondition + | StateCondition + | ScreenCondition + | UserCondition + | OrCondition + | AndCondition; + +interface BaseCondition { + condition: string; +} + +export interface NumericStateCondition extends BaseCondition { + condition: "numeric_state"; + entity?: string; + below?: string | number; + above?: string | number; +} + +export interface StateCondition extends BaseCondition { + condition: "state"; + entity?: string; + state?: string | string[]; + state_not?: string | string[]; +} + +export interface ScreenCondition extends BaseCondition { + condition: "screen"; + media_query?: string; +} + +export interface UserCondition extends BaseCondition { + condition: "user"; + users?: string[]; +} + +export interface OrCondition extends BaseCondition { + condition: "or"; + conditions?: Condition[]; +} + +export interface AndCondition extends BaseCondition { + condition: "and"; + conditions?: Condition[]; +} diff --git a/src/types/homeassistant/panels/lovelave/cards/types.ts b/src/types/homeassistant/panels/lovelace/cards/types.ts similarity index 69% rename from src/types/homeassistant/panels/lovelave/cards/types.ts rename to src/types/homeassistant/panels/lovelace/cards/types.ts index 790046a..d7cee82 100644 --- a/src/types/homeassistant/panels/lovelave/cards/types.ts +++ b/src/types/homeassistant/panels/lovelace/cards/types.ts @@ -1,12 +1,26 @@ import {ActionConfig, LovelaceCardConfig} from "../../../data/lovelace"; +/** + * Home Assistant Area Card Config. + * + * @see https://www.home-assistant.io/dashboards/area/ + */ +export interface AreaCardConfig extends LovelaceCardConfig { + area: string; + navigation_path?: string; + show_camera?: boolean; + camera_view?: "live" | "auto"; + aspect_ratio?: string; +} + /** * Home Assistant Picture Entity Config. * * @property {string} entity An entity_id used for the picture. * @property {string} [name] Overwrite entity name. * @property {string} [image] URL of an image. - * @property {string} [camera_image] Camera entity_id to use. (not required if entity is already a camera-entity). + * @property {string} [camera_image] Camera entity_id to use. + * (not required if the entity is already a camera-entity). * @property {string} [camera_view=auto] “live” will show the live view if stream is enabled. * @property {Record} [state_image] Map entity states to images (state: image URL). * @property {string[]} [state_filter] State-based CSS filters. @@ -39,3 +53,17 @@ export interface PictureEntityCardConfig extends LovelaceCardConfig { show_state?: boolean; theme?: string; } + +/** + * Home Assistant Stack Card Config. + * + * @property {string} type The stack type. + * @property {Object[]} cards The content of the stack. + * + * @see https://www.home-assistant.io/dashboards/horizontal-stack/ + * @see https://www.home-assistant.io/dashboards/vertical-stack/ + */ +export interface StackCardConfig extends LovelaceCardConfig { + cards: LovelaceCardConfig[]; + title?: string; +} diff --git a/src/types/homeassistant/panels/lovelace/types.ts b/src/types/homeassistant/panels/lovelace/types.ts new file mode 100644 index 0000000..6496f32 --- /dev/null +++ b/src/types/homeassistant/panels/lovelace/types.ts @@ -0,0 +1,17 @@ +export interface LovelaceLayoutOptions { + grid_columns?: number | "full"; + grid_rows?: number | "auto"; + grid_max_columns?: number; + grid_min_columns?: number; + grid_min_rows?: number; + grid_max_rows?: number; +} + +export interface LovelaceGridOptions { + columns?: number | "full"; + rows?: number | "auto"; + max_columns?: number; + min_columns?: number; + min_rows?: number; + max_rows?: number; +} diff --git a/src/types/homeassistant/types.ts b/src/types/homeassistant/types.ts index da65978..e61eb32 100644 --- a/src/types/homeassistant/types.ts +++ b/src/types/homeassistant/types.ts @@ -1,7 +1,21 @@ -import {Auth, Connection, HassConfig, HassEntities, HassServices, MessageBase,} from "home-assistant-js-websocket"; +import { + Auth, + Connection, + HassConfig, + HassEntities, + HassEntity, + HassServices, + HassServiceTarget, + MessageBase, +} from "home-assistant-js-websocket"; import {AreaRegistryEntry} from "./data/area_registry"; import {DeviceRegistryEntry} from "./data/device_registry"; import {EntityRegistryDisplayEntry} from "./data/entity_registry"; +import {FloorRegistryEntry} from "./data/floor_registry"; +import {Themes} from "./data/ws-themes"; +import {FrontendLocaleData, getHassTranslations} from "./data/translations"; +import {LocalizeFunc} from "./common/translations/localize"; +import {CoreFrontendUserData} from "./data/frontend"; export interface Credential { auth_provider_type: string; @@ -32,10 +46,7 @@ export interface PanelInfo | null> { config_panel_domain?: string; } -export interface Panels { - [name: string]: PanelInfo; -} - +export type Panels = Record; export interface Translation { nativeName: string; @@ -45,27 +56,40 @@ export interface Translation { export interface TranslationMetadata { fragments: string[]; - translations: { - [lang: string]: Translation; - }; + translations: Record; } -export interface Resources { - [language: string]: Record; +export type TranslationDict = {[key: string]: string}; + +export type Resources = Record>; + +// Currently selected theme and its settings. These are the values stored in local storage. +// Note: These values are not meant to be used at runtime to check whether dark mode is active +// or which theme name to use, as this interface represents the config data for the theme picker. +// The actually active dark mode and theme name can be read from hass.themes. +export interface ThemeSettings { + theme: string; + // Radio box selection for theme picker. Do not use in Lovelace rendering as + // it can be undefined == auto. + // Property hass.themes.darkMode carries effective current mode. + dark?: boolean; + primaryColor?: string; + accentColor?: string; } export interface HomeAssistant { - auth: Auth & { external?: any }; + auth: Auth & { external?: {[key: string]: any;} }; connection: Connection; connected: boolean; states: HassEntities; - entities: { [id: string]: EntityRegistryDisplayEntry }; - devices: { [id: string]: DeviceRegistryEntry }; - areas: { [id: string]: AreaRegistryEntry }; + entities: Record; + devices: Record; + areas: Record; + floors: Record; services: HassServices; config: HassConfig; - themes: { [k: string]: any }; - selectedTheme: { [k: string]: any } | null; + themes: Themes; + selectedTheme: ThemeSettings | null; panels: Panels; panelUrl: string; // i18n @@ -77,9 +101,9 @@ export interface HomeAssistant { language: string; // local stored language, keep that name for backward compatibility selectedLanguage: string | null; - locale: { [k: string]: any }; + locale: FrontendLocaleData; resources: Resources; - localize: Function; + localize: LocalizeFunc; translationMetadata: TranslationMetadata; suspendWhenHidden: boolean; enableShortcuts: boolean; @@ -89,7 +113,61 @@ export interface HomeAssistant { defaultPanel: string; moreInfoEntityId: string | null; user?: CurrentUser; - userData?: { [k: string]: any } | null; - + userData?: CoreFrontendUserData | null; + hassUrl(path?: any): string; + callService( + domain: ServiceCallRequest["domain"], + service: ServiceCallRequest["service"], + serviceData?: ServiceCallRequest["serviceData"], + target?: ServiceCallRequest["target"], + notifyOnError?: boolean, + returnResponse?: boolean + ): Promise; + callApi( + method: "GET" | "POST" | "PUT" | "DELETE", + path: string, + parameters?: Record, + headers?: Record + ): Promise; + callApiRaw( // introduced in 2024.11 + method: "GET" | "POST" | "PUT" | "DELETE", + path: string, + parameters?: Record, + headers?: Record, + signal?: AbortSignal + ): Promise; + fetchWithAuth(path: string, init?: Record): Promise; + sendWS(msg: MessageBase): void; callWS(msg: MessageBase): Promise; + loadBackendTranslation( + category: Parameters[2], + integrations?: Parameters[3], + configFlow?: Parameters[4] + ): Promise; + loadFragmentTranslation(fragment: string): Promise; + formatEntityState(stateObj: HassEntity, state?: string): string; + formatEntityAttributeValue( + stateObj: HassEntity, + attribute: string, + value?: any + ): string; + formatEntityAttributeName(stateObj: HassEntity, attribute: string): string; +} + +export interface Context { + id: string; + parent_id?: string; + user_id?: string | null; +} + +export interface ServiceCallRequest { + domain: string; + service: string; + serviceData?: Record; + target?: HassServiceTarget; +} + +export interface ServiceCallResponse { + context: Context; + response?: any; } diff --git a/src/types/lovelace-mushroom/cards/chips-card.ts b/src/types/lovelace-mushroom/cards/chips-card.ts index fff7b4f..55bf341 100644 --- a/src/types/lovelace-mushroom/cards/chips-card.ts +++ b/src/types/lovelace-mushroom/cards/chips-card.ts @@ -5,7 +5,7 @@ import {LovelaceChipConfig} from "../utils/lovelace/chip/types"; * Chips Card Configuration * * @param {LovelaceChipConfig[]} chips Chips Array - * @param {string} [alignment=start] Chips alignment (end, center, justify), when empty default behavior is start. + * @param {string} [alignment=start] Chips alignment (start,end, center, justify), when empty default behavior is start. * * @see https://github.com/piitaya/lovelace-mushroom/blob/main/docs/cards/chips.md */ diff --git a/src/types/lovelace-mushroom/cards/fan-card-config.ts b/src/types/lovelace-mushroom/cards/fan-card-config.ts index 3dae196..cf792a7 100644 --- a/src/types/lovelace-mushroom/cards/fan-card-config.ts +++ b/src/types/lovelace-mushroom/cards/fan-card-config.ts @@ -9,6 +9,7 @@ import {AppearanceSharedConfig} from "../shared/config/appearance-config"; * @property {boolean} [icon_animation=false] Animate the icon when fan is on. * @property {boolean} [show_percentage_control=false] Show a slider to control speed. * @property {boolean} [show_oscillate_control=false] Show a button to control oscillation. + * @property {boolean} [show_direction_control=false] Show a button to control the direction. * @property {boolean} [icon_animation=false] Animate the icon when fan is on. * * @see https://github.com/piitaya/lovelace-mushroom/blob/main/docs/cards/fan.md @@ -20,5 +21,6 @@ export type FanCardConfig = LovelaceCardConfig & icon_animation?: boolean; show_percentage_control?: boolean; show_oscillate_control?: boolean; + show_direction_control?: boolean; collapsible_controls?: boolean; }; diff --git a/src/types/strategy/cards.ts b/src/types/strategy/cards.ts index a5574a8..20eacfc 100644 --- a/src/types/strategy/cards.ts +++ b/src/types/strategy/cards.ts @@ -5,11 +5,10 @@ import {AppearanceSharedConfig} from "../lovelace-mushroom/shared/config/appeara import {ActionsSharedConfig} from "../lovelace-mushroom/shared/config/actions-config"; import {TemplateCardConfig} from "../lovelace-mushroom/cards/template-card-config"; import {EntityCardConfig} from "../lovelace-mushroom/cards/entity-card-config"; -import {PictureEntityCardConfig} from "../homeassistant/panels/lovelave/cards/types"; +import {AreaCardConfig, PictureEntityCardConfig} from "../homeassistant/panels/lovelace/cards/types"; import {ClimateCardConfig} from "../lovelace-mushroom/cards/climate-card-config"; import {CoverCardConfig} from "../lovelace-mushroom/cards/cover-card-config"; import {FanCardConfig} from "../lovelace-mushroom/cards/fan-card-config"; -import {AreaCardConfig} from "../homeassistant/lovelace/cards/types"; import {LightCardConfig} from "../lovelace-mushroom/cards/light-card-config"; import {LockCardConfig} from "../lovelace-mushroom/cards/lock-card-config"; import {MediaPlayerCardConfig} from "../lovelace-mushroom/cards/media-player-card-config"; diff --git a/src/types/strategy/generic.ts b/src/types/strategy/generic.ts index 94e3838..7ced83d 100644 --- a/src/types/strategy/generic.ts +++ b/src/types/strategy/generic.ts @@ -1,19 +1,98 @@ -import { - CallServiceActionConfig, - LovelaceCardConfig, - LovelaceConfig, - LovelaceViewConfig -} from "../homeassistant/data/lovelace"; +import {CallServiceActionConfig, LovelaceCardConfig,} from "../homeassistant/data/lovelace"; import {HomeAssistant} from "../homeassistant/types"; import {AreaRegistryEntry} from "../homeassistant/data/area_registry"; import {cards} from "./cards"; import {EntityRegistryEntry} from "../homeassistant/data/entity_registry"; import {LovelaceChipConfig} from "../lovelace-mushroom/utils/lovelace/chip/types"; import {HassServiceTarget} from "home-assistant-js-websocket"; +import {LovelaceViewConfig, LovelaceViewRawConfig} from "../homeassistant/data/lovelace/config/view"; +import {LovelaceConfig} from "../homeassistant/data/lovelace/config/types"; + +/** + * List of supported domains. + * + * This constant array defines the domains that are supported by the strategy. + * Each domain represents a specific type of entity within the Home Assistant ecosystem. + * + * _ refers to all domains. + * default refers to the miscellanea domain. + * + * @readonly + * @constant + */ +const SUPPORTED_DOMAINS = [ + "_", + "binary_sensor", + "camera", + "climate", + "cover", + "default", + "fan", + "input_select", + "light", + "lock", + "media_player", + "number", + "scene", + "select", + "sensor", + "switch", + "vacuum", +] as const; + +/** + * List of supported views. + * + * This constant array defines the views that are supported by the strategy. + * + * @readonly + * @constant + */ +const SUPPORTED_VIEWS = [ + "camera", + "climate", + "cover", + "fan", + "home", + "light", + "scene", + "switch", + "vacuum", +] as const; + +const SUPPORTED_CHIPS = [ + "light", + "fan", + "cover", + "switch", + "climate", + "weather", +] as const; + +/** + * List of home view sections. + * + * This constant array defines the sections that are present in the home view. + * + * @readonly + * @constant + */ +const HOME_VIEW_SECTIONS = [ + "areas", + "areasTitle", + "chips", + "greeting", + "persons", +] as const; export namespace generic { + export type SupportedDomains = typeof SUPPORTED_DOMAINS[number]; + export type SupportedViews = typeof SUPPORTED_VIEWS[number]; + export type SupportedChips = typeof SUPPORTED_CHIPS[number]; + export type HomeViewSections = typeof HOME_VIEW_SECTIONS[number]; + /** - * An entry out of a Home Assistant Register. + * An entry of a Home Assistant Register. */ export type RegistryEntry = | AreaRegistryEntry @@ -21,164 +100,179 @@ export namespace generic { | EntityRegistryEntry /** - * View Entity. + * View Configuration of the strategy. * - * @property {number} [order] Ordering position of the entity in the list of available views. - * @property {boolean} [hidden] True if the entity should be hidden from the dashboard. + * @interface StrategyViewConfig + * @extends LovelaceViewConfig + * + * @property {boolean} [hidden] If True, the view is hidden from the dashboard. + * @property {number} [order] Ordering position of the views at the top of the dashboard. */ - export interface ViewConfig extends LovelaceViewConfig { - hidden?: boolean; + export interface StrategyViewConfig extends LovelaceViewConfig { + hidden: boolean; + order: number; + } + + /** + * All Domains Configuration. + * + * @interface AllDomainsConfig + * + * @property {boolean} [hide_config_entities] If True, all configuration entities are hidden from the dashboard. + * @property {boolean} [hide_diagnostic_entities] If True, all diagnostic entities are hidden from the dashboard. + */ + export interface AllDomainsConfig { + hide_config_entities: boolean; + hide_diagnostic_entities: boolean; + } + + /** + * Single Domain Configuration. + * + * @interface SingleDomainConfig + * @extends Partial + * + * @property {boolean} [hidden] If True, all entities of the domain are hidden from the dashboard. + * @property {number} [order] Ordering position of the domains in a views. + */ + export interface SingleDomainConfig extends Partial { + hidden: boolean; order?: number; } /** - * Domain Configuration. - * - * @property {number} [order] Ordering position of the entity in the list of available views. - * @property {boolean} [hidden] True if the entity should be hidden from the dashboard. - * @property {boolean} [hide_config_entities] True if the entity's categorie is "config" and should be hidden from the - * dashboard. - * @property {boolean} [hide_diagnostic_entities] True if the entity's categorie is "diagnostic" and should be hidden - * from the dashboard. - */ - export interface DomainConfig extends Partial { - hidden?: boolean; - order?: number; - hide_config_entities?: boolean - hide_diagnostic_entities?: boolean - } - - /** - * Dashboard Information Object. + * Dashboard Info Object. * * Home Assistant passes this object to the Dashboard Generator method. * + * @interface DashboardInfo + * * @property {LovelaceConfig} config Dashboard configuration. * @property {HomeAssistant} hass The Home Assistant object. * * @see https://developers.home-assistant.io/docs/frontend/custom-ui/custom-strategy/#dashboard-strategies */ - export interface DashBoardInfo { - config?: LovelaceConfig & { + export interface DashboardInfo { + config: LovelaceViewRawConfig & { strategy: { - options?: StrategyConfig + options?: StrategyConfig & { area: StrategyArea } } }; hass: HomeAssistant; } /** - * View Information Object. + * View Info Object. * * Home Assistant passes this object to the View Generator method. * - * @property {LovelaceViewConfig} view View configuration. + * @interface ViewInfo + * * @property {LovelaceConfig} config Dashboard configuration. * @property {HomeAssistant} hass The Home Assistant object. + * @property {LovelaceViewConfig} view View configuration. * * @see https://developers.home-assistant.io/docs/frontend/custom-ui/custom-strategy/#view-strategies */ export interface ViewInfo { - view: LovelaceViewConfig & { - strategy?: { + config: LovelaceConfig; + hass: HomeAssistant; + view: LovelaceViewRawConfig & { + strategy: { options?: StrategyConfig & { area: StrategyArea } } }; - config: LovelaceConfig - hass: HomeAssistant; } /** * Strategy Configuration. * - * @property {Object.} areas List of areas. - * @property {Object.} [card_options] Card options for entities. - * @property {chips} [chips] List of chips to show in the Home view. - * @property {boolean} [debug] Set to true for more verbose debugging info. - * @property {Object.} domains List of domains. - * @property {object[]} [extra_cards] List of cards to show below room cards. - * @property {object[]} [extra_views] List of views to add to the dashboard. - * @property {object[]} [quick_access_cards] List of cards to show between welcome card and rooms cards. - * @property {Object.} views List of views. + * @interface StrategyConfig + * + * @property {Object.} areas List of areas. + * @property {Object.} card_options Card options for entities. + * @property {Partial} chips The configuration of chips in the Home view. + * @property {boolean} debug If True, the strategy outputs more verbose debug information in the console. + * @property {Object.} domains List of domains. + * @property {LovelaceCardConfig[]} extra_cards List of cards to show below room cards. + * @property {StrategyViewConfig[]} extra_views List of custom-defined views to add to the dashboard. + * @property {{ hidden: HomeViewSections[] | [] }} home_view List of views to add to the dashboard. + * @property {Object.} views The configurations of views. + * @property {LovelaceCardConfig[]} quick_access_cards List of custom-defined cards to show between the welcome card + * and rooms cards. */ export interface StrategyConfig { - areas: { [k: string]: StrategyArea }; - card_options?: { [k: string]: CustomCardConfig }; - chips?: Chips; + areas: { [S: string]: StrategyArea }; + card_options: { [S: string]: CustomCardConfig }; + chips: Partial; debug: boolean; - domains: { [k: string]: DomainConfig }; - extra_cards?: LovelaceCardConfig[]; - extra_views?: ViewConfig[]; + domains: { [K in SupportedDomains]: K extends "_" ? AllDomainsConfig : SingleDomainConfig; }; + extra_cards: LovelaceCardConfig[]; + extra_views: StrategyViewConfig[]; home_view: { - hidden: HiddenSectionType[] + hidden: HomeViewSections[] | []; } - quick_access_cards?: LovelaceCardConfig[]; - views: { [k: string]: ViewConfig }; + views: Record; + quick_access_cards: LovelaceCardConfig[]; } - const hiddenSectionList = ["chips", "persons", "greeting", "areas", "areasTitle"] as const; - export type HiddenSectionType = typeof hiddenSectionList[number]; - /** * Represents the default configuration for a strategy. + * + * @interface StrategyDefaults */ export interface StrategyDefaults extends StrategyConfig { - areas: { - undisclosed: StrategyArea & { - area_id: "undisclosed", - }, - [k: string]: StrategyArea, - }, - domains: { - default: DomainConfig, - [k: string]: DomainConfig, - } + areas: { "undisclosed": StrategyArea } & { [S: string]: StrategyArea }; } /** * Strategy Area. * - * @property {number} [order] Ordering position of the area in the list of available areas. + * @interface StrategyArea + * * @property {boolean} [hidden] True if the entity should be hidden from the dashboard. * @property {object[]} [extra_cards] An array of card configurations. * The configured cards are added to the dashboard. - * @property {string} [type=default] The type of area card. + * @property {number} [order] Ordering position of the area in the list of available areas. + * @property {string} [type] The type of area card. */ export interface StrategyArea extends AreaRegistryEntry { - order?: number; - hidden?: boolean; extra_cards?: LovelaceCardConfig[]; + hidden?: boolean; + order?: number; type?: string; } /** * A list of chips to show in the Home view. * - * @property {boolean} light_count Chip to display the number of lights on. - * @property {boolean} fan_count Chip to display the number of fans on. - * @property {boolean} cover_count Chip to display the number of unclosed covers. - * @property {boolean} switch_count Chip to display the number of switches on. + * @interface ChipConfiguration + * * @property {boolean} climate_count Chip to display the number of climates which are not off. + * @property {boolean} cover_count Chip to display the number of unclosed covers. + * @property {boolean} fan_count Chip to display the number of fans on. + * @property {boolean} light_count Chip to display the number of lights on. + * @property {boolean} switch_count Chip to display the number of switches on. * @property {string} weather_entity Entity ID for the weather chip to use, accepts `weather.` only. * @property {object[]} extra_chips List of extra chips. */ - export interface Chips { - extra_chips: LovelaceChipConfig[]; - - light_count: boolean; - fan_count: boolean; - cover_count: boolean; - switch_count: boolean; + export interface ChipConfiguration { climate_count: boolean; + cover_count: boolean; + extra_chips: LovelaceChipConfig[]; + fan_count: boolean; + light_count: boolean; + switch_count: boolean; weather_entity: string; - - [key: string]: any; } /** * Custom Card Configuration for an entity. * - * @property {boolean} hidden True if the entity should be hidden from the dashboard. + * @interface CustomCardConfig + * @extends LovelaceCardConfig + * + * @property {boolean} hidden If True, the card is hidden from the dashboard. */ export interface CustomCardConfig extends LovelaceCardConfig { hidden?: boolean; @@ -187,9 +281,12 @@ export namespace generic { /** * Area Filter Context. * - * @property {AreaRegistryEntry} area Area Entity. - * @property {string[]} areaDeviceIds The id of devices which are linked to the area entity. - * @property {string} [domain] Domain of the entity. Example: `light`. + * @interface AreaFilterContext + * + * @property {AreaRegistryEntry} area Area Entry. + * @property {string[]} areaDeviceIds The id of devices which are linked to the area. + * @property {string} [domain] Domain of an entity. + * Example: `light`. */ export interface AreaFilterContext { area: AreaRegistryEntry; @@ -201,7 +298,7 @@ export namespace generic { * Checks if the given object is an instance of CallServiceActionConfig. * * @param {any} obj - The object to be checked. - * @return {boolean} - Returns true if the object is an instance of CallServiceActionConfig, otherwise false. + * @returns {boolean} - Returns true if the object is an instance of CallServiceActionConfig, otherwise false. */ export function isCallServiceActionConfig(obj: any): obj is CallServiceActionConfig { return obj && obj.action === "call-service" && ["action", "service"].every(key => key in obj); @@ -211,9 +308,60 @@ export namespace generic { * Checks if the given object is an instance of HassServiceTarget. * * @param {any} obj - The object to check. - * @return {boolean} - True if the object is an instance of HassServiceTarget, false otherwise. + * @returns {boolean} - True if the object is an instance of HassServiceTarget, false otherwise. */ export function isCallServiceActionTarget(obj: any): obj is HassServiceTarget { return obj && ["entity_id", "device_id", "area_id"].some(key => key in obj); } + + interface SortableBase { + order: number; + } + + type SortableWithTitle = SortableBase & { title: string; name?: never }; + type SortableWithName = SortableBase & { name: string; title?: never }; + + + /** + * The union type of SortableWithTitle and SortableWithName. + * + * @remarks + * This type is used to sort objects by title or by name. + * The `order` property is used to sort the objects. + * The `title` and `name` properties are used to display the object in the UI. + */ + export type Sortable = SortableWithTitle | SortableWithName; + + + /** + * Checks if the given object is of a sortable type. + * + * Sortable types are objects that have a `title` or a `name` property and an `order` property. + * + * @param {any} obj - The object to check. + * @returns {boolean} - True if the object is an instance of Sortable, false otherwise. + */ + export function isSortable(obj: any): obj is Sortable { + return obj && 'order' in obj && ('title' in obj || 'name' in obj); + } + + /** + * Checks if the given view id is a supported view. + * + * @param {string} id - The view id to check. + * @returns {boolean} - Returns true if the view id is a supported view, otherwise false. + */ + export function isSupportedView(id: string): id is SupportedViews { + return SUPPORTED_VIEWS.includes(id as SupportedViews); + } + + /** + * Checks if the given domain id is a supported domain. + * + * @param {string} id - The domain id to check. + * @returns {boolean} - Returns true if the domain id is a supported domain, otherwise false. + */ + export function isSupportedDomain(id: string): id is SupportedDomains { + return SUPPORTED_DOMAINS.includes(id as SupportedDomains); + } } diff --git a/src/types/strategy/views.ts b/src/types/strategy/views.ts index 99190a4..17442e5 100644 --- a/src/types/strategy/views.ts +++ b/src/types/strategy/views.ts @@ -1,5 +1,5 @@ import {cards} from "./cards"; -import {LovelaceViewConfig} from "../homeassistant/data/lovelace"; +import {LovelaceViewConfig} from "../homeassistant/data/lovelace/config/view"; export namespace views { /** diff --git a/src/utillties/filters.ts b/src/utillties/filters.ts index d2d4b4d..8786d58 100644 --- a/src/utillties/filters.ts +++ b/src/utillties/filters.ts @@ -1,5 +1,7 @@ import {EntityRegistryEntry} from "../types/homeassistant/data/entity_registry"; import {Helper} from "../Helper"; +import {generic} from "../types/strategy/generic"; +import SupportedDomains = generic.SupportedDomains; /** * Filter an array of entities by property/value pair @@ -20,7 +22,7 @@ export function filterEntitiesByPropertyValue( return entities.filter(entity => exclude ? entity[property] !== value : entity[property] === value); } -export function applyEntityCategoryFilters(entities: EntityRegistryEntry[], domain: string) { +export function applyEntityCategoryFilters(entities: EntityRegistryEntry[], domain: SupportedDomains) { if (!Helper.isInitialized()) { throw new Error("The Helper module must be initialized before using this one."); } @@ -49,4 +51,14 @@ export function applyEntityCategoryFilters(entities: EntityRegistryEntry[], doma return entities; } +/*export function filterHiddenEntities(entities: EntityRegistryEntry[]) { + entities = entities.filter( + function (entity) { + return entity.hidden_by === null // entity is not hidden by HASS settings. + && entity.disabled_by === null // entity is not disabled by HASS settings. + && Helper.strategyOptions.card_options.[entity.entity_id] // entity is not hidden by strategy options. + } + ); +}*/ + diff --git a/src/views/AbstractView.ts b/src/views/AbstractView.ts index 6387017..a252f81 100644 --- a/src/views/AbstractView.ts +++ b/src/views/AbstractView.ts @@ -1,12 +1,15 @@ import {Helper} from "../Helper"; import {ControllerCard} from "../cards/ControllerCard"; -import {StackCardConfig} from "../types/homeassistant/lovelace/cards/types"; -import {LovelaceCardConfig, LovelaceViewConfig} from "../types/homeassistant/data/lovelace"; +import {LovelaceCardConfig} from "../types/homeassistant/data/lovelace"; import {cards} from "../types/strategy/cards"; import {TitleCardConfig} from "../types/lovelace-mushroom/cards/title-card-config"; import {HassServiceTarget} from "home-assistant-js-websocket"; import {applyEntityCategoryFilters} from "../utillties/filters"; +import {LovelaceViewConfig} from "../types/homeassistant/data/lovelace/config/view"; +import {StackCardConfig} from "../types/homeassistant/panels/lovelace/cards/types"; +import {generic} from "../types/strategy/generic"; import abstractCardConfig = cards.AbstractCardConfig; +import SupportedDomains = generic.SupportedDomains; /** * Abstract View Class. @@ -40,20 +43,21 @@ abstract class AbstractView { /** * The domain of which we operate the devices. * + * @type {SupportedDomains | "home"} * @private * @readonly */ - readonly #domain: string; + readonly #domain: SupportedDomains | "home"; /** * Class constructor. * - * @param {string} domain The domain which the view is representing. + * @param {SupportedDomains} domain The domain which the view is representing. * * @throws {Error} If trying to instantiate this class. * @throws {Error} If the Helper module isn't initialized. */ - protected constructor(domain: string) { + protected constructor(domain: SupportedDomains | "home") { if (!Helper.isInitialized()) { throw new Error("The Helper module must be initialized before using this one."); } @@ -67,6 +71,13 @@ abstract class AbstractView { * @return {Promise<(StackCardConfig | TitleCardConfig)[]>} An array of card objects. */ async createViewCards(): Promise<(StackCardConfig | TitleCardConfig)[]> { + if (this.#domain === "home") { + // The home domain should override this method because it hasn't entities on its own. + // The method override creates its own cards to show at the home view. + + return []; + } + const viewCards: LovelaceCardConfig[] = []; // Create cards for each area. diff --git a/src/views/CameraView.ts b/src/views/CameraView.ts index 03c83a0..1fd2005 100644 --- a/src/views/CameraView.ts +++ b/src/views/CameraView.ts @@ -3,6 +3,8 @@ import {AbstractView} from "./AbstractView"; import {views} from "../types/strategy/views"; import {cards} from "../types/strategy/cards"; import {Helper} from "../Helper"; +import {generic} from "../types/strategy/generic"; +import SupportedDomains = generic.SupportedDomains; // noinspection JSUnusedGlobalSymbols Class is dynamically imported. /** @@ -17,11 +19,11 @@ class CameraView extends AbstractView { /** * Domain of the view's entities. * - * @type {string} + * @type {SupportedDomains} * @static * @private */ - static #domain: string = "camera"; + static #domain: SupportedDomains = "camera"; /** * Default configuration of the view. diff --git a/src/views/ClimateView.ts b/src/views/ClimateView.ts index e1bf5d0..dfd458a 100644 --- a/src/views/ClimateView.ts +++ b/src/views/ClimateView.ts @@ -3,6 +3,8 @@ import {ControllerCard} from "../cards/ControllerCard"; import {AbstractView} from "./AbstractView"; import {views} from "../types/strategy/views"; import {cards} from "../types/strategy/cards"; +import {generic} from "../types/strategy/generic"; +import SupportedDomains = generic.SupportedDomains; // noinspection JSUnusedGlobalSymbols Class is dynamically imported. /** @@ -17,11 +19,11 @@ class ClimateView extends AbstractView { /** * Domain of the view's entities. * - * @type {string} + * @type {SupportedDomains} * @static * @private */ - static #domain: string = "climate"; + static #domain: SupportedDomains = "climate"; /** * Default configuration of the view. diff --git a/src/views/CoverView.ts b/src/views/CoverView.ts index 4ac4dee..de576e4 100644 --- a/src/views/CoverView.ts +++ b/src/views/CoverView.ts @@ -3,6 +3,8 @@ import {ControllerCard} from "../cards/ControllerCard"; import {AbstractView} from "./AbstractView"; import {views} from "../types/strategy/views"; import {cards} from "../types/strategy/cards"; +import {generic} from "../types/strategy/generic"; +import SupportedDomains = generic.SupportedDomains; // noinspection JSUnusedGlobalSymbols Class is dynamically imported. /** @@ -17,11 +19,11 @@ class CoverView extends AbstractView { /** * Domain of the view's entities. * - * @type {string} + * @type {SupportedDomains} * @static * @private */ - static #domain: string = "cover"; + static #domain: SupportedDomains = "cover"; /** * Default configuration of the view. diff --git a/src/views/FanView.ts b/src/views/FanView.ts index 09b9aab..3cdd259 100644 --- a/src/views/FanView.ts +++ b/src/views/FanView.ts @@ -3,6 +3,8 @@ import {ControllerCard} from "../cards/ControllerCard"; import {AbstractView} from "./AbstractView"; import {views} from "../types/strategy/views"; import {cards} from "../types/strategy/cards"; +import {generic} from "../types/strategy/generic"; +import SupportedDomains = generic.SupportedDomains; // noinspection JSUnusedGlobalSymbols Class is dynamically imported. /** @@ -17,11 +19,11 @@ class FanView extends AbstractView { /** * Domain of the view's entities. * - * @type {string} + * @type {SupportedDomains} * @static * @private */ - static #domain: string = "fan"; + static #domain: SupportedDomains = "fan"; /** * Default configuration of the view. diff --git a/src/views/HomeView.ts b/src/views/HomeView.ts index 3f9011f..2106f71 100644 --- a/src/views/HomeView.ts +++ b/src/views/HomeView.ts @@ -3,11 +3,13 @@ import {AbstractView} from "./AbstractView"; import {views} from "../types/strategy/views"; import {LovelaceChipConfig} from "../types/lovelace-mushroom/utils/lovelace/chip/types"; import {ChipsCardConfig} from "../types/lovelace-mushroom/cards/chips-card"; -import {AreaCardConfig, StackCardConfig} from "../types/homeassistant/lovelace/cards/types"; import {TemplateCardConfig} from "../types/lovelace-mushroom/cards/template-card-config"; import {ActionConfig} from "../types/homeassistant/data/lovelace"; import {TitleCardConfig} from "../types/lovelace-mushroom/cards/title-card-config"; import {PersonCardConfig} from "../types/lovelace-mushroom/cards/person-card-config"; +import {AreaCardConfig, StackCardConfig} from "../types/homeassistant/panels/lovelace/cards/types"; +import {generic} from "../types/strategy/generic"; +import supportedChips = generic.SupportedChips; // noinspection JSUnusedGlobalSymbols Class is dynamically imported. @@ -76,27 +78,26 @@ class HomeView extends AbstractView { } as StackCardConfig); } - if (!Helper.strategyOptions.home_view.hidden.includes("greeting")) { - const greeting = - homeViewCards.push({ - type: "custom:mushroom-template-card", - primary: - `{% set time = now().hour %} {% if (time >= 18) %} ${Helper.customLocalize("generic.good_evening")},{{user}}! - {% elif (time >= 12) %} ${Helper.customLocalize("generic.good_afternoon")}, {{user}}! - {% elif (time >= 5) %} ${Helper.customLocalize("generic.good_morning")}, {{user}}! - {% else %} ${Helper.customLocalize("generic.hello")}, {{user}}! {% endif %}`, - icon: "mdi:hand-wave", - icon_color: "orange", - tap_action: { - action: "none", - } as ActionConfig, - double_tap_action: { - action: "none", - } as ActionConfig, - hold_action: { - action: "none", - } as ActionConfig, - } as TemplateCardConfig); + if (!(Helper.strategyOptions.home_view.hidden as string[]).includes("greeting")) { + homeViewCards.push({ + type: "custom:mushroom-template-card", + primary: + `{% set time = now().hour %} {% if (time >= 18) %} ${Helper.customLocalize("generic.good_evening")},{{user}}! + {% elif (time >= 12) %} ${Helper.customLocalize("generic.good_afternoon")}, {{user}}! + {% elif (time >= 5) %} ${Helper.customLocalize("generic.good_morning")}, {{user}}! + {% else %} ${Helper.customLocalize("generic.hello")}, {{user}}! {% endif %}`, + icon: "mdi:hand-wave", + icon_color: "orange", + tap_action: { + action: "none", + } as ActionConfig, + double_tap_action: { + action: "none", + } as ActionConfig, + hold_action: { + action: "none", + } as ActionConfig, + } as TemplateCardConfig); } // Add quick access cards. @@ -125,8 +126,8 @@ class HomeView extends AbstractView { * @return {Promise} Promise a chip array. */ async #createChips(): Promise { - if (Helper.strategyOptions.home_view.hidden.includes("chips")) { - // Chips section is hidden. + if ((Helper.strategyOptions.home_view.hidden as string[]).includes("chips")) { + // The Chip section is hidden. return []; } @@ -135,14 +136,14 @@ class HomeView extends AbstractView { const chipOptions = Helper.strategyOptions.chips; // TODO: Get domains from config. - const exposedChips = ["light", "fan", "cover", "switch", "climate"]; + const exposedChips: supportedChips[] = ["light", "fan", "cover", "switch", "climate"]; // Create a list of area-ids, used for switching all devices via chips const areaIds = Helper.areas.map(area => area.area_id ?? ""); let chipModule; // Weather chip. - const weatherEntityId = chipOptions?.weather_entity ?? Helper.entities.find( + const weatherEntityId = chipOptions.weather_entity ?? Helper.entities.find( (entity) => entity.entity_id.startsWith("weather.") && entity.disabled_by === null && entity.hidden_by === null, )?.entity_id; @@ -159,7 +160,7 @@ class HomeView extends AbstractView { // Numeric chips. for (let chipType of exposedChips) { - if (chipOptions?.[`${chipType}_count` as string] ?? true) { + if (chipType !== "weather" && (chipOptions?.[(`${chipType}_count`)] ?? true)) { const className = Helper.sanitizeClassName(chipType + "Chip"); try { chipModule = await import((`../chips/${className}`)); @@ -187,8 +188,8 @@ class HomeView extends AbstractView { * @return {PersonCardConfig[]} A Person Card array. */ #createPersonCards(): PersonCardConfig[] { - if (Helper.strategyOptions.home_view.hidden.includes("persons")) { - // Person section is hidden. + if ((Helper.strategyOptions.home_view.hidden as string[]).includes("persons")) { + // The Person section is hidden. return []; } @@ -216,7 +217,7 @@ class HomeView extends AbstractView { * @return {Promise<(TitleCardConfig | StackCardConfig)[]>} Promise an Area Card Section. */ async #createAreaSection(): Promise<(TitleCardConfig | StackCardConfig)[]> { - if (Helper.strategyOptions.home_view.hidden.includes("areas")) { + if ((Helper.strategyOptions.home_view.hidden as string[]).includes("areas")) { // Areas section is hidden. return []; @@ -226,7 +227,7 @@ class HomeView extends AbstractView { let areaCards: (TemplateCardConfig | AreaCardConfig)[] = []; - if (!Helper.strategyOptions.home_view.hidden.includes("areasTitle")) { + if (!(Helper.strategyOptions.home_view.hidden as string[]).includes("areasTitle")) { groupedCards.push({ type: "custom:mushroom-title-card", title: Helper.customLocalize("generic.areas"), diff --git a/src/views/LightView.ts b/src/views/LightView.ts index 7f5cab6..90f2cde 100644 --- a/src/views/LightView.ts +++ b/src/views/LightView.ts @@ -3,6 +3,8 @@ import {ControllerCard} from "../cards/ControllerCard"; import {AbstractView} from "./AbstractView"; import {views} from "../types/strategy/views"; import {cards} from "../types/strategy/cards"; +import {generic} from "../types/strategy/generic"; +import SupportedDomains = generic.SupportedDomains; // noinspection JSUnusedGlobalSymbols Class is dynamically imported. /** @@ -17,11 +19,11 @@ class LightView extends AbstractView { /** * Domain of the view's entities. * - * @type {string} + * @type {SupportedDomains} * @static * @private */ - static #domain: string = "light"; + static #domain: SupportedDomains = "light"; /** * Default configuration of the view. diff --git a/src/views/SceneView.ts b/src/views/SceneView.ts index 54a66ce..613352e 100644 --- a/src/views/SceneView.ts +++ b/src/views/SceneView.ts @@ -1,6 +1,8 @@ import {Helper} from "../Helper"; import {AbstractView} from "./AbstractView"; import {views} from "../types/strategy/views"; +import {generic} from "../types/strategy/generic"; +import SupportedDomains = generic.SupportedDomains; // noinspection JSUnusedGlobalSymbols Class is dynamically imported. /** @@ -15,11 +17,11 @@ class SceneView extends AbstractView { /** * Domain of the view's entities. * - * @type {string} + * @type {SupportedDomains} * @static * @private */ - static #domain: string = "scene"; + static #domain: SupportedDomains = "scene"; /** * Default configuration of the view. diff --git a/src/views/SwitchView.ts b/src/views/SwitchView.ts index 88d9622..9df4c07 100644 --- a/src/views/SwitchView.ts +++ b/src/views/SwitchView.ts @@ -3,6 +3,8 @@ import {ControllerCard} from "../cards/ControllerCard"; import {AbstractView} from "./AbstractView"; import {views} from "../types/strategy/views"; import {cards} from "../types/strategy/cards"; +import {generic} from "../types/strategy/generic"; +import SupportedDomains = generic.SupportedDomains; // noinspection JSUnusedGlobalSymbols Class is dynamically imported. /** @@ -17,11 +19,11 @@ class SwitchView extends AbstractView { /** * Domain of the view's entities. * - * @type {string} + * @type {SupportedDomains} * @static * @private */ - static #domain: string = "switch"; + static #domain: SupportedDomains = "switch"; /** * Default configuration of the view. diff --git a/src/views/VacuumView.ts b/src/views/VacuumView.ts index b2885a5..819e932 100644 --- a/src/views/VacuumView.ts +++ b/src/views/VacuumView.ts @@ -3,6 +3,8 @@ import {ControllerCard} from "../cards/ControllerCard"; import {AbstractView} from "./AbstractView"; import {views} from "../types/strategy/views"; import {cards} from "../types/strategy/cards"; +import {generic} from "../types/strategy/generic"; +import SupportedDomains = generic.SupportedDomains; // noinspection JSUnusedGlobalSymbols Class is dynamically imported. /** @@ -17,11 +19,11 @@ class VacuumView extends AbstractView { /** * Domain of the view's entities. * - * @type {string} + * @type {SupportedDomains} * @static * @private */ - static #domain: string = "vacuum"; + static #domain: SupportedDomains = "vacuum"; /** * Default configuration of the view.