diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ccbe46 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/node_modules/ diff --git a/README.md b/README.md index 6b5ba70..24d3926 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Mushroom dashboard strategy -[![hacs][hacs-badge]][hacs-url] -[![release][release-badge]][release-url] +[![hacs][hacsBadge]][hacsUrl] +[![release][releaseBadge]][releaseUrl] ![Preview GIF](./docs/preview.gif) @@ -11,48 +11,52 @@ ![customizable](./docs/customizable.png) -## What is Mushroom dashboard strategy ? +## What is Mushroom dashboard strategy? -Mushroom dashboard strategy provides a strategy for Home assistant to automatically create a dashboard using Mushroom cards, the area configuration and entity configuration. +Mushroom dashboard strategy provides a strategy for Home assistant to automatically create a dashboard using Mushroom +cards, the area configuration and entity configuration. -My goal is to propose a way to create powerful dashaboards without the need of spending hours manualy creating them. +My goal is to propose a way to create powerful dashboards without the need of spending hours manually creating them. -**Note:** This is my first javascript code and github repository. Any recomendations are always welcome +**Note:** This is my first javascript code and GitHub repository. Any recommendations are always welcome. ### Features -- 🛠 Automatically create dashboard with 3 lines of yaml -- 😍 Built-in Views for device specific controls -- 🎨 Many options to customize to your needs +- 🛠 Automatically create dashboard with 3 lines of yaml. +- 😍 Built-in Views for device-specific controls. +- 🎨 Many options to customize to fit your needs. ## Installation -### Preresquisites +### Prerequisites -You need to install these cards first before using this strategy -- [Mushroom cards][mushroom] -- [Mini graph card][mini-graph] -- [Web RTC][webrtc] +You need to install these cards before using this strategy: + +- [Mushroom cards][mushroomUrl] +- [Mini graph card][mini-graphUrl] +- [Web RTC][webRtcUrl] ### HACS -Mushroom dashboard strategy is available in [HACS][hacs] (Home Assistant Community Store). +Mushroom dashboard strategy is available in [HACS][hacsUrl] (Home Assistant Community Store). -1. Install HACS if you don't have it already -2. Open HACS in Home Assistant -3. Go to "Frontend" section -4. Click 3 dots on top right and custom repository -5. Add `https://github.com/AalianKhan/mushroom-strategy` with catagory `Lovelace` -5. Search for "Mushroom strategy" and install +1. Install HACS if you don't have it already. +2. Open HACS in Home Assistant. +3. Go to the "Frontend" section. +4. Click the button with the "+" icon +5. Search for "Mushroom dashboard" and install. ### Manual -1. Download `mushroom-strategy.js` file from the [`dist`](https://github.com/AalianKhan/mushroom-strategy/tree/main/dist) directory. +1. Download `mushroom-strategy.js` file from + the [`dist`](https://github.com/AalianKhan/mushroom-strategy/tree/main/dist) directory. 2. Put `mushroom-strategy.js` file into your `config/www` folder. -3. Add reference to `mushroom-strategy.js` in Dashboard. There's two way to do that: - - **Using UI:** _Settings_ → _Dashboards_ → _More Options icon_ → _Resources_ → _Add Resource_ → Set _Url_ as `/local/mushroom-strategy.js` → Set _Resource type_ as `JavaScript Module`. +3. Add a reference to `mushroom-strategy.js` in Dashboard. + There are two ways to do that: + - **Using UI:** _Settings_ → _Dashboards_ → _More Options icon_ → _Resources_ → _Add Resource_ → Set _Url_ + as `/local/mushroom-strategy.js` → Set _Resource type_ as `JavaScript Module`. **Note:** If you do not see the Resources menu, you will need to enable _Advanced Mode_ in your _User Profile_ - - **Using YAML:** Add following code to `lovelace` section. + - **Using YAML:** Add following code to the `lovelace` section. ```yaml resources: - url: /local/mushroom-strategy.js @@ -61,80 +65,60 @@ Mushroom dashboard strategy is available in [HACS][hacs] (Home Assistant Communi ## Usage -All the Rounded cards can be configured using Dashboard UI editor. +All the rounded cards can be configured using the Dashboard UI editor. -1. In Dashboard UI, click 3 dots in top right corner. +1. In the UI of the dashboard, click the 3 dots in the top right corner. 2. Click _Edit Dashboard_. 3. Click 3 dots again 4. Click `Raw configuration editor` -5. Add these lines +5. Add the following lines: + ```yaml strategy: type: custom:mushroom-strategy -views: [] +views: [ ] ``` ### Hidding specific entities -When first creating this dashboard, you probably have many entities that you don't want to see. +When creating this dashboard for the first time, you probably have many entities that you don't want to see. -You can easily hide these entities by holding the entity > Click the `cog icon` at the top right corner of the popup > Click `Advanced settings` > Set `entity status` to `hidden`. Refresh the page and it should update +You can easily hide these entities by holding the entity > Click the `cog icon` in the top right corner of the popup > +Click `Advanced settings` > Set `entity status` to `hidden`. +The view should update when the page is refreshed. ![Views](./docs/Hidden.png) ### Adding devices to areas -You can easiy add devices to an area by going to `Settings` found at the bottom of the sidebar > Click `Devices and integration` > Select the integration of your device > Click the device you wish to add > Click the `pencil icon` found at the top right corner > Enter an area in area field. You can also set an entity of that device to a different area by going to advanced settings of that entity. +You can easily add devices to an area by going to `Settings` found at the bottom of the sidebar > +Click `Devices and integration` > Select the integration of your device > Click the device you wish to add > Click +the `pencil icon` found in the top right corner > Enter an area in area field. +You can also set an entity of that device to a different area by going to the advanced settings of that entity. -If you created a entity in your `configuratation.yaml` you may need to enter a `unique_id` first before you set an area to it. See [docs](https://www.home-assistant.io/faq/unique_id/) +If you created an entity in your `configuration.yaml` you may need to enter a `unique_id` first before you set an area +to it. +See [docs](https://www.home-assistant.io/faq/unique_id/) ## Strategy options -You can set strategy options to further customize the dashboard. It has the following available options -| Name | Type | Default | Description | -|:---------------------|:-----------------------|:--------------------------------------------------------|:-----------------------------------------------------------------------------------------| -| `areas` | list | Optional | One or more areas in a list, see [areas object](#area-object) | -| `entity_config` | list of cards | Optional | Card defination for an entity, see [entity config](#entity-config) | -| `views` | object | All views enabled | Setting which pre-built views to show, see available [Pre-built views](#pre-built-views) | -| `chips` | object | All count chips enabled with auto selected weather card | See [chips](#chips) | -| `quick_access_cards` | list of cards | Optional | List of cards to show between welcome card and rooms cards | -| `extra_cards` | list of cards | Optional | List of cards to show below room cards | -| `extra_views` | list of view | Optional | List of views to add to the dashboard | +You can set strategy options to further customize the dashboard. +By default, all views are enabled which include lights, fans, covers, switches, climates and cameras. All chips are also +enabled which count the number of devices on for the platforms light, fan, cover and climate. It also auto-selects a +weather entity for the weather chip. -#### Example +The options available are: -```yaml -strategy: - type: custom:mushroom-strategy - options: - areas: - - name: Family Room - icon: mdi:sofa - icon_color: green -views: [] -``` -### Area Object - -The area object includes all options from the template mushroom card and `extra_cards` which is a list of cards to show at the top of the area subview. The order of defination is used to sort the rooms and pre-built views - -| Name | Type | Default | Description | -| :-------------------- | :-------------- | :---------- | :---------------------------------------------------------------------------------------------------------------------------------- | -| `name` | string | Required | The name of the area | -| `icon` | string | Optional | Icon to render. May contain [templates](https://www.home-assistant.io/docs/configuration/templating/). | -| `icon_color` | string | Optional | Icon color to render. May contain [templates](https://www.home-assistant.io/docs/configuration/templating/). | -| `primary` | string | Optional | Primary info to render. May contain [templates](https://www.home-assistant.io/docs/configuration/templating/). | -| `secondary` | string | Optional | Secondary info to render. May contain [templates](https://www.home-assistant.io/docs/configuration/templating/). | -| `badge_icon` | string | Optional | Badge icon to render. May contain [templates](https://www.home-assistant.io/docs/configuration/templating/). | -| `badge_color` | string | Optional | Badge icon color to render. May contain [templates](https://www.home-assistant.io/docs/configuration/templating/). | -| `picture` | string | Optional | Picture to render. May contain [templates](https://www.home-assistant.io/docs/configuration/templating/). | -| `multiline_secondary` | boolean | `false` | Enables support for multiline text for the secondary info. | -| `layout` | string | Optional | Layout of the card. Vertical, horizontal and default layout are supported | -| `fill_container` | boolean | `false` | Fill container or not. Useful when card is in a grid, vertical or horizontal layout | -| `tap_action` | action | `none` | Home assistant action to perform on tap | -| `hold_action` | action | `none` | Home assistant action to perform on hold | -| `entity_id` | `string` `list` | Optional | Only reacts to the state changes of these entities. This can be used if the automatic analysis fails to find all relevant entities. | -| `double_tap_action` | action | `more-info` | Home assistant action to perform on double_tap | -| `extra_cards` | list of cards | Optional | A list of cards to show on the top of the area subview | +| Name | Type | Default | Description | +|:---------------------|:--------------------------|:--------------------------------------------------------|:--------------------------------------------------------------------| +| `areas` | object (optional) | unset | One or more areas in a list, see [areas object](#area-object). | +| `entity_config` | array of cards (optional) | unset | Card definition for an entity, see [entity config](#entity-config). | +| `views` | object (optional) | All default views | See available [Pre-built views](#pre-built-views). | +| `chips` | object | All count chips enabled with auto selected weather card | See [chips](#chips). | +| `quick_access_cards` | array of cards (optional) | unset | List of cards to show between welcome card and rooms cards. | +| `extra_cards` | array of cards (optional | unset | List of cards to show below room cards. | +| `extra_views` | array of views (optional) | unset | List of views to add to the dashboard. | +| `domains` | object (optional) | All supported domains | See [Supported domains](#supported-domains). | #### Example @@ -143,28 +127,85 @@ strategy: type: custom:mushroom-strategy options: areas: - - name: Family Room - icon: mdi:television - icon_color: green - extra_cards: - - type: custom:mushroom-chips-card - chips: - - type: entity - entity: sensor.family_room_temperature - icon: mdi:thermometer - icon_color: pink - alignment: center - - name: Kitchen - icon: mdi:silverware-fork-knife - icon_color: red -views: [] + family_room_id: + name: Family Room + icon: mdi:sofa + icon_color: green +views: [ ] ``` +### Area Object + +The area object includes all options from the template mushroom card and `extra_cards` which is a list of cards to show +at the top of the area subview. + +| Name | Type | Default | Description | +|:----------------------|:------------------|:---------------|:------------------------------------------------------------------------------------------------------------------------------------| +| `name` | string | N.A. | The name of the area. | +| `icon` | string (optional) | unset or empty | Icon to render. May contain [templates](https://www.home-assistant.io/docs/configuration/templating/). | +| `icon_color` | string (optional) | unset or empty | Icon color to render. May contain [templates](https://www.home-assistant.io/docs/configuration/templating/). | +| `primary` | string (optional) | unset or empty | Primary info to render. May contain [templates](https://www.home-assistant.io/docs/configuration/templating/). | +| `secondary` | string (optional) | unset or empty | Secondary info to render. May contain [templates](https://www.home-assistant.io/docs/configuration/templating/). | +| `badge_icon` | string (optional) | unset or empty | Badge icon to render. May contain [templates](https://www.home-assistant.io/docs/configuration/templating/). | +| `badge_color` | string (optional) | unset or empty | Badge icon color to render. May contain [templates](https://www.home-assistant.io/docs/configuration/templating/). | +| `picture` | string (optional) | unset or empty | Picture to render. May contain [templates](https://www.home-assistant.io/docs/configuration/templating/). | +| `multiline_secondary` | boolean | `false` | Enables support for multiline text for the secondary info. | +| `layout` | string (optional) | unset or empty | Layout of the card. Vertical, horizontal and default layout are supported. | +| `fill_container` | boolean | `false` | Fill container or not. Useful when card is in a grid, vertical or horizontal layout. | +| `tap_action` | action* | `none` | Home assistant action to perform on tap. | +| `hold_action` | action* | `none` | Home assistant action to perform on hold. | +| `entity_id` | `string` `array` | unset or empty | Only reacts to the state changes of these entities. This can be used if the automatic analysis fails to find all relevant entities. | +| `double_tap_action` | action* | `more-info` | Home assistant action to perform on double_tap. | +| `hidden` | boolean | false | Set to `true` to exclude the area from the dashboard and views. | +| `order` | number | Infinity | Ordering position of the area in the list of available areas. | +| `extra_cards` | array of cards | unset or empty | A list of cards to show on the top of the area subview. | + +*) `more-info` `toggle` `call-service` `navigate` `url` `none` + +#### Example + +```yaml +strategy: + type: custom:mushroom-strategy + options: + areas: + family_room_id: + name: Family Room + icon: mdi:television + icon_color: green + order: 1 + extra_cards: + - type: custom:mushroom-chips-card + chips: + - type: entity + entity: sensor.family_room_temperature + icon: mdi:thermometer + icon_color: pink + alignment: center + kitchen_id: + name: Kitchen + icon: mdi:silverware-fork-knife + icon_color: red + order: 2 + garage_id: + hidden: true +views: [ ] +``` + +#### Undisclosed Area + +The strategy has a special area, named `undisclosed`. +This area is enabled by default and includes the entities that aren't linked to any Home Assistant area. + +The area can be configured like any other area as described above. +To exclude this area from the dashboard and views, set its property `hidden` to `true`. + ### Entity Config -The `entity_config` essentially enables you to give a specific entity any card you wish. +The `entity_config` essentially enables you to give a specific entity any card you wish. #### Example + ```yaml strategy: type: custom:mushroom-strategy @@ -172,23 +213,40 @@ strategy: entity_config: - entity: fan.master_bedroom_fan type: custom:mushroom-fan-card -views: [] +views: [ ] ``` ### Pre-built views ![Light Views](./docs/light_view.png) -Mushroom strategy includes pre-built views to control/view specific domains. Only the devices that are in an area defined in `areas` are shown. If `areas` is not defined then devices in all areas are shown. By default, all views are shown +Mushroom strategy includes pre-built views to control/view specific domains. +Only devices that are in an area as defined in `areas` are shown. +If `areas` is undefined then the devices of all areas are shown. -| Available views | type | Description | -|:----------------|:-----|:------------| -| `lights` | boolean | View to control all lights and lights of each area | -| `fans` | boolean | View to control all fans and fans of each area | -| `covers` | boolean | View to control all covers and covers of each area | -| `switches` | boolean | View to control all switches and switches of each area | -| `climates` | boolean | View to control climate devices such as thermostats. Seperated by each area | -| `cameras` | boolean | View to show all cameras using WebRTC cards. Seperated by each area | +By default, all pre-built views below are shown: + +| Available views | type | Description | +|:----------------|:--------|:-----------------------------------------------------------------------------| +| `light` | object* | View to control all lights and lights of each area. | +| `fan` | object* | View to control all fans and fans of each area. | +| `cover` | object* | View to control all covers and covers of each area. | +| `switch` | object* | View to control all switches and switches of each area. | +| `climate` | object* | View to control climate devices such as thermostats. Seperated by each area. | +| `camera` | object* | View to show all cameras using WebRTC cards. Seperated by each area. | + +*) See [View Options](#view-options). + +#### View Options + +For each of the pre-built views, the following options are available: + +| name | type | description | +|:---------|:--------|:----------------------------------------------------------------------------------------------| +| `title` | string | Title of the view in the navigation bar. (Shown when no icon is defined or hovering above it. | +| `icon` | string | Icon of the view in the navigation bar. | +| `order` | string | Ordering position of the view in the navigation bar. | +| `hidden` | boolean | Set to `true` to exclude the view from the dashboard | #### Example @@ -197,31 +255,63 @@ strategy: type: custom:mushroom-strategy options: views: - lights: true - switches: true - covers: false - cameras: true - climates: false -views: [] + light: + order: 0 + title: illumination + switch: + order: 1 + hidden: true + icon: mdi:toggle-switch +views: [ ] ``` +### Supported domains + +The following domains are supported and enabled by default: + +* light +* fan +* cover +* switch +* camera +* climate +* media_player +* sensor +* binary_sensor +* default (Miscellaneous) + +For these domains, the following options are supported: + +| Option | type | Description | +|:---------------|:--------|:---------------------------------------------------------------------------| +| `title` | string | Title of the domain in a view. | +| `showControls` | boolean | Weather to show controls int a view, to switch all entities of the domain. | +| `hidden` | boolean | Set to `true` to exclude the view from the dashboard. | +| `order` | number | Ordering position of the domain entities in a view. | + ### Chips - ![Chips](./docs/chips.png) -Mushroom strategy has chips that count the number of devices active for a specific domain. Only the devices that are defined in `areas` are counted. if `areas` is not defined then devices in all areas are counted. By default, all chips are enabled. You can also manually configure a weather entity to use. There is also an option to add more [Mushroom Chips][mushroom-chips] using `extra_chips` -##### Note: setting the status to hidden for the unwanted weather entity is recomended +Mushroom strategy has chips that indicate the number of devices which are active for a specific domain. +Only devices of an area as defined in `areas` are counted. +If `areas` is not defined then the devices in all areas are counted. +By default, all chips are enabled. -| Available chips | type | Description | -|:----------------|:--------|:------------------------------------------------------------------------------------------------------------------| -| `light_count` | Boolean | Chip to display the number of lights on, tapping turns off all lights, holding navigates to lights view | -| `fan_count` | Boolean | Chip to display the number of fans on, tapping turns off all fans, holding navigates to fans view | -| `cover_count` | Boolean | Chip to display the number of covers not closed, tapping navigates to covers view | -| `switch_count` | Boolean | Chip to display the number of switches on, tapping turns off all switches, holding navigates to switches view | -| `climate_count` | Boolean | Chip to display the number of climate not off, tapping naviagetes to climates view | -| `weather_entity` | Entity ID | Entity ID for the weather chip to use, accepts `weather.` only | -| `extra_chips` | List | List of extra chips to display, see [Mushroom Chips][mushroom-chips] | +You can manually configure a weather entity-id to use, and there's also an option to add +more [Mushroom Chips][mushroom-chipsUrl] using `extra_chips`. + +**Note: To hide the weather chip, you should hide or disable the entity itself.** + +| Available chips | type | Description | +|:-----------------|:------------------|:---------------------------------------------------------------------------------------------------------------| +| `light_count` | boolean | Chip to display the number of lights on, tapping turns off all lights, holding navigates to lights view. | +| `fan_count` | boolean | Chip to display the number of fans on, tapping turns off all fans, holding navigates to fans view. | +| `cover_count` | boolean | Chip to display the number of covers not closed, tapping navigates to covers view. | +| `switch_count` | boolean | Chip to display the number of switches on, tapping turns off all switches, holding navigates to switches view. | +| `climate_count` | boolean | Chip to display the number of climate not off, tapping navigates to climates view. | +| `weather_entity` | string (optional) | Entity ID for the weather chip to use, accepts `weather.` only. | +| `extra_chips` | array (optional) | List of extra chips to display, see [Mushroom Chips][mushroom-chipsUrl]. | #### Example @@ -254,11 +344,11 @@ strategy: type: custom:mushroom-strategy options: views: - lights: true - switches: true - covers: false - cameras: true - thermostats: false + light: + title: illumination + switches: + hidden: true + icon: mdi:toggle-switch chips: weather_entity: weather.forecast_home climate_count: false @@ -289,7 +379,8 @@ strategy: tap_action: action: toggle areas: - - name: Family Room + family_room_id: + name: Family Room icon: mdi:television icon_color: green extra_cards: @@ -300,25 +391,33 @@ strategy: icon: mdi:thermometer icon_color: pink alignment: center - - name: Kitchen + kitchen_id: + name: Kitchen icon: mdi:silverware-fork-knife icon_color: red - - name: Master Bedroom + master_bedroom_id: + name: Master Bedroom icon: mdi:bed-king icon_color: blue - - name: Abia's Bedroom + abias_bedroom_id: + name: Abia's Bedroom icon: mdi:flower-tulip icon_color: green - - name: Aalian's Bedroom + aalians_bedroom_id: + name: Aalian's Bedroom icon: mdi:rocket-launch icon_color: yellow - - name: Rohaan's Bedroom + rohaans_bedroom_id: + name: Rohaan's Bedroom icon: mdi:controller icon_color: red - - name: Hallway - - name: Living Room + hallway_id: + name: Hallway + living_room_id: + name: Living Room icon: mdi:sofa - - name: Front Door + front_door_id: + name: Front Door icon: mdi:door-closed entity_config: - entity: fan.master_bedroom_fan @@ -349,34 +448,40 @@ strategy: title: cool view path: cool-view icon: mdi:emoticon-cool - badges: [] + badges: [ ] cards: - type: markdown content: I am cool ``` - ## Credits -* The cards used are from [Mushroom][mushroom], [Mini graph card][mini-graph] and [Web RTC][webrtc] -* Took inspiration from [Balloob battery strategy][balloobBattery] +* The cards used are from [Mushroom][mushroomUrl], [Mini graph card][mini-graphUrl] and [WebRTC][webRtcUrl] +* Took inspiration from [Balloob battery strategy][balloobBatteryUrl] - +## Contributors -[hacs-url]: https://github.com/hacs/integration -[hacs-badge]: https://img.shields.io/badge/HACS-Custom-41BDF5.svg -[release-badge]: https://img.shields.io/github/v/release/lovelace-rounded/ui?style=flat-square -[downloads-badge]: https://img.shields.io/github/downloads/lovelace-rounded/ui/total?style=flat-square -[build-badge]: https://img.shields.io/github/actions/workflow/status/lovelace-rounded/ui/build.yml?branch=main&style=flat-square +* [DigiLive](https://github.com/DigiLive) - + + +[hacsBadge]: https://img.shields.io/badge/HACS-Default-41BDF5.svg + +[releaseBadge]: https://img.shields.io/github/v/release/AalianKhan/mushroom-strategy + + + +[hacsUrl]: https://hacs.xyz + +[releaseUrl]: https://github.com/AalianKhan/mushroom-strategy/releases + +[mushroomUrl]: https://github.com/piitaya/lovelace-mushroom + +[mushroom-chipsUrl]: https://github.com/piitaya/lovelace-mushroom/blob/main/docs/cards/chips.md + +[mini-graphUrl]: https://github.com/kalkih/mini-graph-card + +[webRtcUrl]: https://github.com/AlexxIT/WebRTC + +[balloobBatteryUrl]: https://gist.github.com/balloob/4a70c83287ddba4e9085cb578ffb161f -[home-assistant]: https://www.home-assistant.io/ -[home-assitant-theme-docs]: https://www.home-assistant.io/integrations/frontend/#defining-themes -[hacs]: https://hacs.xyz -[mushroom]: https://github.com/piitaya/lovelace-mushroom -[mushroom-chips]: https://github.com/piitaya/lovelace-mushroom/blob/main/docs/cards/chips.md -[mini-graph]: https://github.com/kalkih/mini-graph-card -[webrtc]: https://github.com/AlexxIT/WebRTC -[balloobBattery]: https://gist.github.com/balloob/4a70c83287ddba4e9085cb578ffb161f -[release-url]: https://github.com/AalianKhan/mushroom-strategy/releases diff --git a/dist/mushroom-strategy.js b/dist/mushroom-strategy.js index 118f750..9ee25e1 100644 --- a/dist/mushroom-strategy.js +++ b/dist/mushroom-strategy.js @@ -1,1335 +1 @@ -const getFilteredEntitiesFromEntityRegistry = (entities, devices, area, startsWith) => { - const areaDevices = new Set(); - // Find all devices linked to this area - for (const device of devices) { - if (device.area_id === area.area_id) { - areaDevices.add(device.id); - } - } - - // Filter entities - const filteredEntities = new Set(); - for (const entity of entities) - { - if ((areaDevices.has(entity.device_id) || entity.area_id === area.area_id) && entity.entity_id.startsWith(startsWith) && entity.hidden_by == null && entity.disabled_by == null) - { - filteredEntities.add(entity); - } - } - return filteredEntities; -}; - -const getFilteredEntitiesFromStates = (info, entities, devices, area, startsWith) => { - const entityLookup = Object.fromEntries( - entities.map((ent) => [ent.entity_id, ent]) - ); - const deviceLookup = Object.fromEntries( - devices.map((dev) => [dev.id, dev]) - ); - - let states = Object.values(info.hass.states).filter((stateObj) => - stateObj.entity_id.startsWith(startsWith) - ); - const areaEntities = new Set; - for (const stateObj of states) { - const entry = entityLookup[stateObj.entity_id]; - if (!entry) { - continue; - } - if (entry.area_id) { - if (entry.area_id !== area.area_id) { - continue; - } - } else if (entry.device_id) { - const device = deviceLookup[entry.device_id]; - if (!device || device.area_id !== area.area_id) { - continue; - } - } else { - continue; - } - areaEntities.add(stateObj); - } - return areaEntities; -} - -const createTitleWithControls = (title, subtitle, offService, onService, iconOff, iconOn, area_id) => ( - { - type: "horizontal-stack", - cards: - [ - { - type: "custom:mushroom-title-card", - title: title, - subtitle: subtitle - }, - { - type: "horizontal-stack", - cards: - [ - { - type: "custom:mushroom-template-card", - icon: iconOff, - layout: "vertical", - icon_color: "red", - tap_action: - { - action: "call-service", - service: offService, - target: - { - area_id: area_id - }, - data: {} - }, - }, - { - type: "custom:mushroom-template-card", - icon: iconOn, - layout: "vertical", - icon_color: "amber", - tap_action: - { - action: "call-service", - service: onService, - target: - { - area_id: area_id - }, - data: {} - }, - } - ] - - } - ] - } -) -const createPlatformCard = (entities, entity_config, defaultCard, titleCard, doubleTapActionConfig) => { - const platformCards = []; - if (titleCard != null) { - platformCards.push(titleCard); - } - - entitiesLoop: - for (const entity of entities) - { - // Entity config does not exist then push defualt card, otherwise loop to find matching entity - if (entity_config == null) { - if (doubleTapActionConfig != null) { - var doubleTapAction = - { - double_tap_action: - { - target: - { - entity_id: entity.entity_id - }, - ...doubleTapActionConfig - } - } - } - platformCards.push - ( - { - entity: entity.entity_id, - ...defaultCard, - ...doubleTapAction - } - ) - } else - { - for (const config of entity_config) - { - if (entity.entity_id == config.entity) - { - platformCards.push - ( - { - ...config - }, - ); - continue entitiesLoop; - } - } - - if (doubleTapActionConfig != null) - { - var doubleTapAction = - { - double_tap_action: - { - target: - { - entity_id: entity.entity_id - }, - ...doubleTapActionConfig - } - } - } - - platformCards.push - ( - { - entity: entity.entity_id, - ...defaultCard, - ...doubleTapAction - } - ) - - } - } - - return platformCards; -} - -const createListOfFilteredStates = (entities, devices, definedAreas, startsWith) => { - const filteredEntities = new Set(); - for (const area of definedAreas) - { - const areaDevices = new Set(); - // Find all devices linked to this area - for (const device of devices) { - if (device.area_id === area.area_id) { - areaDevices.add(device.id); - } - } - - // Filter entities - for (const entity of entities) - { - if ((areaDevices.has(entity.device_id) || entity.area_id === area.area_id) && entity.entity_id.startsWith(startsWith) && entity.hidden_by == null && entity.disabled_by == null) - { - filteredEntities.add(entity); - } - } - } - - // create a list of states.light - var statesList = []; - for (const entity of filteredEntities) - { - statesList.push - ( - "states['" + entity.entity_id + "']" - ); - } - return statesList -} - -class MushroomStrategy { - - static async generateDashboard(info) - { - const strategyOptions = info.config.strategy.options || {}; - // Query all data we need. We will make it available to views by storing it in strategy options. - const [areas, devices, entities] = await Promise.all([ - info.hass.callWS({ type: "config/area_registry/list" }), - info.hass.callWS({ type: "config/device_registry/list" }), - info.hass.callWS({ type: "config/entity_registry/list" }), - ]); - - // Create People card for each person - let people = Object.values(info.hass.states).filter((stateObj) => - stateObj.entity_id.startsWith("person.") - ); - const peopleCards = []; - for (const person of people) - { - peopleCards.push - ( - { - type: "custom:mushroom-person-card", - layout: "vertical", - primary_info: "none", - secondary_info: "none", - icon_type: "entity-picture", - entity: person.entity_id - }, - ); - } - /******************************************************* - ***** Create Room cards for each area in Home view ***** - *******************************************************/ - const roomCards = []; - - // Find all user defined areas and push the card, if not defined, create the room card for every area - const definedAreas = new Set(); - if (strategyOptions.areas != null) - { - for (const userDefinedArea of strategyOptions.areas) - { - for (const area of areas) - { - if (userDefinedArea.name == area.name) - { - definedAreas.add(area); - roomCards.push - ( - { - type: "custom:mushroom-template-card", - primary: area.name, - icon: "mdi:texture-box", - icon_color: "blue", - tap_action: - { - action: "navigate", - navigation_path: area.area_id - }, - ...userDefinedArea, - }, - ); - } - } - } - } else - { - for (const area of areas) - { - definedAreas.add(area); - roomCards.push - ( - { - type: "custom:mushroom-template-card", - primary: area.name, - icon: "mdi:texture-box", - icon_color: "blue", - tap_action: - { - action: "navigate", - navigation_path: area.area_id - }, - }, - ); - } - } - - // horizontally stack the room cards, 2 per row - const horizontalRoomcards = []; - for (var i = 0; i < roomCards.length; i = i + 2) - { - if (roomCards[i+1] == null) - { - horizontalRoomcards.push( - { - type: "horizontal-stack", - cards: - [ - roomCards[i] - ] - } - ) - } else - { - horizontalRoomcards.push( - { - type: "horizontal-stack", - cards: - [ - roomCards[i], - roomCards[i+1], - ] - } - ) - } - } - - // Create list of area ids, used for turning off all devices via chips - const area_ids = []; - for (const area of definedAreas) - { - area_ids.push(area.area_id); - } - - /******************************************************************************** - ***** Create chip to show how many are on for each platform if not disabled ***** - ********************************************************************************/ - const chips = [] - - // weather - if (strategyOptions.chips != null && strategyOptions.chips.weather_entity != null) - { - chips.push - ( - { - type: "weather", - entity: strategyOptions.chips.weather_entity, - show_temperature: true, - show_conditions: true - } - ) - } else - { - const weatherEntity = entities.find(entity => entity.entity_id.startsWith("weather.") && entity.disabled_by == null && entity.hidden_by == null) - if (weatherEntity != null) - { - chips.push - ( - { - type: "weather", - entity: weatherEntity.entity_id, - show_temperature: true, - show_conditions: true - } - ) - } - - } - - - // Light count - const lightCountTemplate = "{% set lights = [" + createListOfFilteredStates(entities, devices, definedAreas, "light.") + "] %} {{ lights | selectattr('state','eq','on') | list | count }}"; - if (strategyOptions.chips == null || (strategyOptions.chips != null && strategyOptions.chips.light_count != false)) - { - chips.push - ( - { - type: "template", - icon: "mdi:lightbulb", - icon_color: "amber", - content: lightCountTemplate, - tap_action: - { - action: "call-service", - service: "light.turn_off", - target: - { - area_id: area_ids - }, - data: {} - }, - hold_action: - { - action: "navigate", - navigation_path: "lights" - } - }, - ) - } - - // Fan count - const fanCountTemplate = "{% set fans = [" + createListOfFilteredStates(entities, devices, definedAreas, "fan.") + "] %} {{ fans | selectattr('state','eq','on') | list | count }}"; - if (strategyOptions.chips == null || (strategyOptions.chips != null && strategyOptions.chips.fan_count != false)) - { - chips.push - ( - { - type: "template", - icon: "mdi:fan", - icon_color: "green", - content: fanCountTemplate, - tap_action: - { - action: "call-service", - service: "fan.turn_off", - target: - { - area_id: area_ids - }, - data: {} - }, - hold_action: - { - action: "navigate", - navigation_path: "fans" - } - } - ) - } - - // Cover count - const coverCountTemplate = "{% set covers = [" + createListOfFilteredStates(entities, devices, definedAreas, "cover.") + "]%} {{ covers | selectattr('state','eq','open') | list | count }}" - if (strategyOptions.chips == null || (strategyOptions.chips != null && strategyOptions.chips.cover_count != false)) - { - chips.push - ( - { - type: "template", - icon: "mdi:window-open", - icon_color: "cyan", - content: coverCountTemplate, - tap_action: - { - action: "navigate", - navigation_path: "covers" - } - }, - ) - } - - // Switch count - const switchCountTemplate = "{% set switches = [" + createListOfFilteredStates(entities, devices, definedAreas, "switch.") + "] %} {{ switches | selectattr('state','eq','on') | list | count }}"; - if (strategyOptions.chips == null || (strategyOptions.chips != null && strategyOptions.chips.switch_count != false)) - { - chips.push - ( - { - type: "template", - icon: "mdi:power-plug", - icon_color: "blue", - content: switchCountTemplate, - tap_action: - { - action: "call-service", - service: "switch.turn_off", - target: - { - area_id: area_ids - }, - data: {} - }, - hold_action: - { - action: "navigate", - navigation_path: "switches" - } - }, - ) - } - - - // Thermostat count - const thermostatCountTemplate = "{% set thermostats = [" + createListOfFilteredStates(entities, devices, definedAreas, "climate.") + "]%} {{ thermostats | selectattr('state','ne','off') | list | count }}" - if (strategyOptions.chips == null || (strategyOptions.chips != null && strategyOptions.chips.climate_count != false)) - { - chips.push - ( - { - type: "template", - icon: "mdi:thermostat", - icon_color: "orange", - content: thermostatCountTemplate, - tap_action: - { - action: "navigate", - navigation_path: "thermostats" - } - }, - ) - } - - // Extra cards - if (strategyOptions.chips != null && strategyOptions.chips.extra_chips != null) { - chips.push - ( - ...strategyOptions.chips.extra_chips - ) - } - - - /*************************** - ***** Create Home view ***** - ***************************/ - const homeViewcards = []; - homeViewcards.push - ( - { - type: "custom:mushroom-chips-card", - alignment: "center", - chips: chips - }, - { - type: "horizontal-stack", - cards: peopleCards - }, - { - type: "custom:mushroom-template-card", - primary: "{% set time = now().hour %} {% if (time >= 18) %} Good Evening, {{user}}! {% elif (time >= 12) %} Good Afternoon, {{user}}! {% elif (time >= 5) %} Good Morning, {{user}}! {% else %} Hello, {{user}}! {% endif %}", - icon: "mdi:hand-wave", - icon_color: "orange" - } - ); - - if (strategyOptions.quick_access_cards != null) { - homeViewcards.push(...strategyOptions.quick_access_cards); - } - - homeViewcards.push - ( - { - type: "custom:mushroom-title-card", - title: "Rooms" - }, - { - type: "vertical-stack", - cards: horizontalRoomcards - } - ); - - if (strategyOptions.extra_cards != null) { - homeViewcards.push(...strategyOptions.extra_cards); - } - - const Views = []; - Views.push( - { - title: "Home", - path: "home", - cards: homeViewcards - }, - ); - - // Create Subview for each user defined areas - const entity_config = strategyOptions.entity_config; - const defined_areas = strategyOptions.areas; - for (const area of definedAreas) - { - Views.push - ( - { - title: area.name, - path: area.area_id, - subview: true, - strategy: { - type: "custom:mushroom-strategy", - options: { area, devices, entities, entity_config, defined_areas }, - } - }, - ); - } - - /*************************************** - ***** Create Light view if enabled ***** - ***************************************/ - if (strategyOptions.views == null || (strategyOptions.views != null && strategyOptions.views.lights != false)) - { - const lightViewCards = []; - lightViewCards.push(createTitleWithControls("All Lights", lightCountTemplate + " lights on", "light.turn_off", "light.turn_on", "mdi:lightbulb-off", "mdi:lightbulb", area_ids )); - for (const area of definedAreas) - { - const lights = getFilteredEntitiesFromEntityRegistry(entities, devices, area, "light."); - // If there are lights, create a title card and a light card for each lights - if (lights.size > 0) - { - lightViewCards.push( - { - type: "vertical-stack", - cards: createPlatformCard( - lights, - entity_config, - { - type: "custom:mushroom-light-card", - show_brightness_control: true, - show_color_control: true, - use_light_color: true - }, - createTitleWithControls(area.name, null, "light.turn_off", "light.turn_on", "mdi:lightbulb-off", "mdi:lightbulb", area.area_id), - { - action: "call-service", - service: "light.turn_on", - data: - { - rgb_color: - [ - 255, - 255, - 255 - ] - } - } - ) - }, - ) - } - } - - // Add the light view to Views - Views.push - ( - { - title: "Lights", - path: "lights", - icon: "mdi:lightbulb-group", - cards: lightViewCards - } - ); - } - - /************************************* - ***** Create Fan view if enabled ***** - *************************************/ - if (strategyOptions.views == null || (strategyOptions.views != null && strategyOptions.views.fans != false)) - { - const fanViewCards = []; - fanViewCards.push(createTitleWithControls("All Fans", fanCountTemplate + " fans on", "fan.turn_off", "fan.turn_on", "mdi:fan-off", "mdi:fan", area_ids )); - for (const area of definedAreas) - { - const fans = getFilteredEntitiesFromEntityRegistry(entities, devices, area, "fan."); - if (fans.size > 0) - { - fanViewCards.push( - { - type: "vertical-stack", - cards: createPlatformCard( - fans, - entity_config, - { - type: "custom:mushroom-fan-card", - show_percentage_control: true, - show_oscillate_control: true, - icon_animation: true - }, - createTitleWithControls(area.name, null, "fan.turn_off", "fan.turn_on", "mdi:fan-off", "mdi:fan", area.area_id) - ) - }, - ) - } - } - - // Add the light view to Views - Views.push - ( - { - title: "Fans", - path: "fans", - icon: "mdi:fan", - cards: fanViewCards - } - ); - } - - /**************************************** - ***** Create Covers view if enabled ***** - ****************************************/ - if (strategyOptions.views == null || (strategyOptions.views != null && strategyOptions.views.covers != false)) - { - const coverViewCards = []; - coverViewCards.push(createTitleWithControls("All Covers", coverCountTemplate + " covers open", "cover.close_cover", "cover.open_cover", "mdi:arrow-down", "mdi:arrow-up", area_ids )); - for (const area of definedAreas) - { - const covers = getFilteredEntitiesFromEntityRegistry(entities, devices, area, "cover."); - if (covers.size > 0) - { - coverViewCards.push - ( - { - type: "vertical-stack", - cards: createPlatformCard( - covers, - entity_config, - { - type: "custom:mushroom-cover-card", - show_buttons_control: true, - show_position_control: true, - show_tilt_position_control: true - }, - createTitleWithControls(area.name, null, "cover.close_cover", "cover.open_cover", "mdi:arrow-down", "mdi:arrow-up", area.area_id) - ) - }, - ) - } - } - - // Add the switch view to Views - Views.push - ( - { - title: "Covers", - path: "covers", - icon: "mdi:window-open", - cards: coverViewCards - } - ); - } - - /****************************************** - ***** Create Switches view if enabled ***** - ******************************************/ - if (strategyOptions.views == null || (strategyOptions.views != null && strategyOptions.views.switches != false)) - { - const switchViewCards = []; - switchViewCards.push(createTitleWithControls("All Switches", switchCountTemplate + " switches on", "switch.turn_off","switch.turn_on", "mdi:power-plug-off", "mdi:power-plug", area_ids )); - for (const area of definedAreas) - { - const switches = getFilteredEntitiesFromEntityRegistry(entities, devices, area, "switch."); - if (switches.size > 0) - { - switchViewCards.push - ( - { - type: "vertical-stack", - cards: createPlatformCard( - switches, - entity_config, - { - type: "custom:mushroom-entity-card", - tap_action: - { - action: "toggle" - } - }, - createTitleWithControls(area.name, null, "switch.turn_off","switch.turn_on", "mdi:power-plug-off", "mdi:power-plug", area.area_id) - ) - }, - ) - } - } - - // Add the switch view to Views - Views.push - ( - { - title: "Switches", - path: "switches", - icon: "mdi:dip-switch", - cards: switchViewCards - } - ); - } - - /****************************************** - ***** Create Climate view if enabled ***** - ******************************************/ - if (strategyOptions.views == null || (strategyOptions.views != null && strategyOptions.views.climates != false)) - { - const thermostatViewCards = []; - thermostatViewCards.push( - { - - type: "custom:mushroom-title-card", - title: "Thermostats", - subtitle: thermostatCountTemplate + " thermostats on", - - } - ); - for (const area of definedAreas) - { - const thermostats = getFilteredEntitiesFromEntityRegistry(entities, devices, area, "climate."); - if (thermostats.size > 0) - { - thermostatViewCards.push - ( - { - type: "vertical-stack", - cards: createPlatformCard( - thermostats, - entity_config, - { - type: "custom:mushroom-climate-card", - hvac_modes: - [ - "off", - "cool", - "heat", - "fan_only" - ], - show_temperature_control: true - }, - { - type: "custom:mushroom-title-card", - title: area.name - } - ) - }, - ) - } - } - - // Add the switch view to Views - Views.push - ( - { - title: "Thermostats", - path: "thermostats", - icon: "mdi:thermostat", - cards: thermostatViewCards - } - ); - } - - /**************************************** - ***** Create camera view if enabled ***** - ****************************************/ - if (strategyOptions.views == null || (strategyOptions.views != null && strategyOptions.views.cameras != false)) - { - const cameraViewCards = []; - cameraViewCards.push( - { - - type: "custom:mushroom-title-card", - title: "Cameras", - } - ); - for (const area of definedAreas) - { - const cameraAreaCard = []; - const cameras = getFilteredEntitiesFromEntityRegistry(entities, devices, area, "camera."); - // If there are cameras, create a title card and a camera card for each cameras - if (cameras.size > 0) - { - cameraAreaCard.push - ( - { - type: "custom:mushroom-title-card", - title: area.name, - }, - ); - - for (const camera of cameras) - { - cameraAreaCard.push - ( - { - type: "custom:webrtc-camera", - entity: camera.entity_id - }, - ); - } - } - - cameraViewCards.push - ( - { - type: "vertical-stack", - cards: cameraAreaCard - }, - ) - } - - // Add the camera view to Views - Views.push - ( - { - title: "Cameras", - path: "cameras", - icon: "mdi:cctv", - cards: cameraViewCards - } - ); - } - - // Add extra views if defined - if (strategyOptions.extra_views != null) { - Views.push - ( - ...strategyOptions.extra_views - ) - } - - // Return views - return { - views: Views - }; - } - - static async generateView(info) - { - // Get all required values - const area = info.view.strategy.options.area; - const devices = info.view.strategy.options.devices; - const entities = info.view.strategy.options.entities - const entity_config = info.view.strategy.options.entity_config - const definedAreas = info.view.strategy.options.defined_areas - - - - const cards = []; - // Add extra cards if defined - if (definedAreas != null) { - for (const definedArea of definedAreas) - { - if (definedArea.name == area.name && definedArea.extra_cards != null) - { - cards.push(...definedArea.extra_cards); - } - } - - } - - // Create light cards - const lights = getFilteredEntitiesFromEntityRegistry(entities, devices, area, "light."); - if (lights.size > 0) - { - cards.push - ( - { - type: "vertical-stack", - cards: createPlatformCard( - lights, - entity_config, - { - type: "custom:mushroom-light-card", - show_brightness_control: true, - show_color_control: true, - use_light_color: true - }, - createTitleWithControls(null, "Lights", "light.turn_off", "light.turn_on", "mdi:lightbulb-off", "mdi:lightbulb", area.area_id), - { - action: "call-service", - service: "light.turn_on", - data: - { - rgb_color: - [ - 255, - 255, - 255 - ] - } - }) - } - ) - - } - // Create fan cards - const fans = getFilteredEntitiesFromEntityRegistry(entities, devices, area, "fan."); - if (fans.size > 0) - { - cards.push - ( - { - type: "vertical-stack", - cards: - createPlatformCard( - fans, - entity_config, - { - type: "custom:mushroom-fan-card", - show_percentage_control: true, - show_oscillate_control: true, - icon_animation: true - }, - createTitleWithControls(null, "Fans", "fan.turn_off", "fan.turn_on", "mdi:fan-off", "mdi:fan", area.area_id)) - } - ) - } - - // Create cover cards - const covers = getFilteredEntitiesFromEntityRegistry(entities, devices, area, "cover."); - if (covers.size > 0) - { - cards.push - ( - { - type: "vertical-stack", - cards: - createPlatformCard( - covers, - entity_config, - { - type: "custom:mushroom-cover-card", - show_buttons_control: true, - show_position_control: true, - show_tilt_position_control: true - }, - createTitleWithControls(null, "Covers", "cover.close_cover", "cover.open_cover", "mdi:arrow-down", "mdi:arrow-up", area.area_id) - ) - } - ) - } - - // Create switch cards - const switches = getFilteredEntitiesFromEntityRegistry(entities, devices, area, "switch."); - if (switches.size > 0) - { - cards.push - ( - { - type: "vertical-stack", - cards: - createPlatformCard( - switches, - entity_config, - { - type: "custom:mushroom-entity-card", - tap_action: - { - action: "toggle" - } - }, - createTitleWithControls(null, "Switches", "switch.turn_off", "switch.turn_off", "mdi:power-plug-off", "mdi:power-plug", area.area_id) - ) - } - ) - } - - // Create climate cards - const thermoststats = getFilteredEntitiesFromEntityRegistry(entities, devices, area, "climate."); - if (thermoststats.size > 0) - { - cards.push - ( - { - type: "vertical-stack", - cards: - createPlatformCard( - thermoststats, - entity_config, - { - type: "custom:mushroom-climate-card", - hvac_modes: - [ - "off", - "cool", - "heat", - "fan_only" - ], - show_temperature_control: true - }, - { - type: "custom:mushroom-title-card", - subtitle: "Climate" - } - ) - } - ) - } - - // Create Media player cards - const media_players = getFilteredEntitiesFromEntityRegistry(entities, devices, area, "media_player."); - if (media_players.size > 0) - { - cards.push - ( - { - type: "vertical-stack", - cards: - createPlatformCard( - media_players, - entity_config, - { - type: "custom:mushroom-media-player-card", - use_media_info: true, - media_controls: - [ - "on_off", - "play_pause_stop" - ], - show_volume_level: true, - volume_controls: - [ - "volume_mute", - "volume_set", - "volume_buttons" - ] - }, - { - type: "custom:mushroom-title-card", - subtitle: "Media Players" - } - ) - } - ) - } - - // Create Sensor cards - const sensorsStateObj = getFilteredEntitiesFromStates(info, entities, devices, area, "sensor."); - const sensors = getFilteredEntitiesFromEntityRegistry(entities, devices, area, "sensor."); - if (sensors.size > 0) - { - const sensorCards = [] - sensorCards.push - ( - { - type: "custom:mushroom-title-card", - subtitle: "Sensors" - }, - ); - sensorsLoop: - for (const sensor of sensors) - { - // Find the state obj that matches with current sensor - var sensorStateObj; - for (const stateObj of sensorsStateObj) - { - if (stateObj.entity_id == sensor.entity_id) - { - sensorStateObj = stateObj; - } - } - - if (entity_config == null) { - if (sensorStateObj.attributes.unit_of_measurement != null) - { - sensorCards.push - ( - { - type: "custom:mini-graph-card", - entities: - [ - sensor.entity_id - ], - animate: true, - line_color: "green" - }, - ); - } else - { - sensorCards.push - ( - { - type: "custom:mushroom-entity-card", - entity: sensor.entity_id, - icon_color: "green" - }, - ); - } - } else - { - for (const config of entity_config) - { - if (sensor.entity_id == config.entity_id) - { - sensorCards.push - ( - { - ...config - }, - ); - continue sensorsLoop; - } - } - - if (sensorStateObj.attributes.unit_of_measurement != null) - { - sensorCards.push - ( - { - type: "custom:mini-graph-card", - entities: - [ - sensor.entity_id - ], - animate: true, - line_color: "green" - }, - ); - } else - { - sensorCards.push - ( - { - type: "custom:mushroom-entity-card", - entity: sensor.entity_id, - icon_color: "green" - }, - ); - } - } - } - cards.push - ( - { - type: "vertical-stack", - cards: sensorCards - }, - ) - } - - // Create card for binary sensors - const binary_sensors = getFilteredEntitiesFromEntityRegistry(entities, devices, area, "binary_sensor."); - if (binary_sensors.size > 0) - { - const binarySensorCards = createPlatformCard - ( - binary_sensors, - entity_config, - { - type: "custom:mushroom-entity-card", - icon_color: "green" - }, - null - ) - const horizontalBinarySensorcards = []; - horizontalBinarySensorcards.push - ( - { - type: "custom:mushroom-title-card", - subtitle: "Binary Sensors" - } - ) - for (var i = 0; i < binarySensorCards.length; i = i + 2) - { - if (binarySensorCards[i+1] == null) - { - horizontalBinarySensorcards.push( - { - type: "horizontal-stack", - cards: - [ - binarySensorCards[i] - ] - } - ) - } else - { - horizontalBinarySensorcards.push( - { - type: "horizontal-stack", - cards: - [ - binarySensorCards[i], - binarySensorCards[i+1], - ] - } - ) - } - } - - cards.push - ( - { - type: "vertical-stack", - cards: horizontalBinarySensorcards - } - ) - } - - // Create card of miscelnanous, I am engilsh professional - const areaDevices = new Set(); - // Find all devices linked to this area - for (const device of devices) { - if (device.area_id === area.area_id) { - areaDevices.add(device.id); - } - } - - // Filter entities - const others = new Set(); - for (const entity of entities) - { - if ((areaDevices.has(entity.device_id) || entity.area_id === area.area_id) && entity.hidden_by == null && entity.disabled_by == null && - !entity.entity_id.startsWith("light.") && - !entity.entity_id.startsWith("fan.") && - !entity.entity_id.startsWith("cover.") && - !entity.entity_id.startsWith("switch.") && - !entity.entity_id.startsWith("climate.") && - !entity.entity_id.startsWith("sensor.") && - !entity.entity_id.startsWith("binary_sensor.") && - !entity.entity_id.startsWith("media_player.") - ) - { - others.add(entity); - } - } - if (others.size > 0) - { - cards.push - ( - { - type: "vertical-stack", - cards: - createPlatformCard( - others, - entity_config, - { - type: "custom:mushroom-entity-card", - icon_color: "blue-grey" - }, - { - type: "custom:mushroom-title-card", - subtitle: "More" - } - ) - } - ) - } - - // Return cards - return { - cards - }; - } -} - -customElements.define("ll-strategy-mushroom-strategy", MushroomStrategy); +(()=>{var t,e,i={84:(t,e,i)=>{"use strict";i.d(e,{W:()=>a});const s={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}},r={aliases:[],area_id:null,name:"Undisclosed",picture:null,hidden:!1},o={default:{title:"Miscellaneous",showControls:!1,hidden:!1},light:{title:"Lights",showControls:!0,iconOn:"mdi:lightbulb",iconOff:"mdi:lightbulb-off",onService:"light.turn_on",offService:"light.turn_off",hidden:!1},fan:{title:"Fans",showControls:!0,iconOn:"mdi:fan",iconOff:"mdi:fan-off",onService:"fan.turn_on",offService:"fan.turn_off",hidden:!1},cover:{title:"Covers",showControls:!0,iconOn:"mdi:arrow-up",iconOff:"mdi:arrow-down",onService:"cover.open_cover",offService:"cover.close_cover",hidden:!1},switch:{title:"Switches",showControls:!0,iconOn:"mdi:power-plug",iconOff:"mdi:power-plug-off",onService:"switch.turn_on",offService:"switch.turn_off",hidden:!1},camera:{title:"Cameras",showControls:!1,hidden:!1},climate:{title:"Climates",showControls:!1,hidden:!1},media_player:{title:"Media Players",showControls:!1,hidden:!1},sensor:{title:"Sensors",showControls:!1,hidden:!1},binary_sensor:{title:"Binary Sensors",showControls:!1,hidden:!1}};class a{static#t;static#e;static#i=[];static#s;static#r=!1;static#o={};static debug=false;constructor(){throw new Error("This class should be invoked with method initialize() instead of using the keyword new!")}static get strategyOptions(){return this.#o}static get areas(){return this.#i}static get devices(){return this.#e}static get entities(){return this.#t}static get debug(){return this.debug}static async initialize(t){this.#s=t.hass.states;try{[this.#t,this.#e,this.#i]=await Promise.all([t.hass.callWS({type:"config/entity_registry/list"}),t.hass.callWS({type:"config/device_registry/list"}),t.hass.callWS({type:"config/area_registry/list"})])}catch(t){console.error(a.debug?t:"An error occurred while querying Home assistant's registries!")}this.#o=structuredClone(t.config.strategy.options||{}),this.debug=this.#o.debug,this.#o.areas=this.#o.areas??{},this.#o.views=this.#o.views??{},this.#o.domains=this.#o.domains??{},this.#o.areas.undisclosed?.hidden||(this.#o.areas.undisclosed={...r,...this.#o.areas.undisclosed},this.#o.areas.undisclosed.area_id=null,this.#i.push(this.#o.areas.undisclosed)),this.#i=a.areas.map((t=>({...t,...this.#o.areas[t.area_id??"undisclosed"]}))),this.#i.sort(((t,e)=>(t.order??1/0)-(e.order??1/0)||t.name.localeCompare(e.name)));for(const t of Object.keys(s))this.#o.views[t]={...s[t],...this.#o.views[t]};this.#o.views=Object.fromEntries(Object.entries(this.#o.views).sort((([,t],[,e])=>(t.order??1/0)-(e.order??1/0)||t.title?.localeCompare(e.title))));for(const t of Object.keys(o))this.#o.domains[t]={...o[t],...this.#o.domains[t]};this.#o.domains=Object.fromEntries(Object.entries(this.#o.domains).sort((([,t],[,e])=>(t.order??1/0)-(e.order??1/0)||t.title?.localeCompare(e.title)))),this.#r=!0}static isInitialized(){return this.#r}static getCountTemplate(t,e,i){const s=[];this.isInitialized()||console.warn("Helper class should be initialized before calling this method!");for(const e of this.#i){const i=this.#e.filter((t=>t.area_id===e.area_id)).map((t=>t.id)),r=this.#t.filter(this.#a,{area:e,domain:t,areaDeviceIds:i}).map((t=>`states['${t.entity_id}']`));s.push(...r)}return`{% set entities = [${s}] %} {{ entities | selectattr('state','${e}','${i}') | list | count }}`}static#a(t){return(this.area.area_id?this.areaDeviceIds.includes(t.device_id)||t.area_id===this.area.area_id:(this.areaDeviceIds.includes(t.device_id)||!t.device_id)&&!t.area_id)&&t.entity_id.startsWith(`${this.domain}.`)&&null==t.hidden_by&&null==t.disabled_by}static getDeviceEntities(t,e){this.isInitialized()||console.warn("Helper class should be initialized before calling this method!");const i=this.#e.filter((e=>e.area_id===t.area_id)).map((t=>t.id));return this.#t.filter(this.#a,{area:t,domain:e,areaDeviceIds:i}).sort(((t,e)=>t.original_name?.localeCompare(e.original_name)))}static getStateEntities(t,e){this.isInitialized()||console.warn("Helper class should be initialized before calling this method!");const i=[],s=Object.fromEntries(this.#t.map((t=>[t.entity_id,t]))),r=Object.fromEntries(this.#e.map((t=>[t.id,t]))),o=Object.values(this.#s).filter((t=>t.entity_id.startsWith(`${e}.`)));for(const e of o){const o=s[e.entity_id],a=r[o?.device_id];(o?.area_id===t.area_id||a&&a.area_id===t.area_id)&&i.push(e)}return i}static sanitizeClassName(t){return(t=t.charAt(0).toUpperCase()+t.slice(1)).replace(/([-_][a-z])/g,(t=>t.toUpperCase().replace("-","").replace("_","")))}static#n(t,e,i){const s=[];for(const r of Object.keys(t))t[r][e]===i&&s.push(r);return s}static getExposedViewIds(){return this.isInitialized()||console.warn("Helper class should be initialized before calling this method!"),this.#n(this.#o.views,"hidden",!1)}static getExposedDomainIds(){return this.isInitialized()||console.warn("Helper class should be initialized before calling this method!"),this.#n(this.#o.domains,"hidden",!1)}}},981:(t,e,i)=>{"use strict";i.r(e),i.d(e,{AbstractCard:()=>r});var s=i(84);class r{entity;options={type:"custom:mushroom-entity-card",icon:"mdi:help-circle",double_tap_action:{action:null}};constructor(t){if(this.constructor===r)throw new Error("Abstract classes can't be instantiated.");if(!s.W.isInitialized())throw new Error("The Helper module must be initialized before using this one.");this.entity=t}mergeOptions(t,e){this.options={...this.options,...t,...e};try{this.options.double_tap_action.target.entity_id=this.entity.entity_id}catch{}}getCard(){return{entity:this.entity.entity_id,...this.options}}}},138:(t,e,i)=>{"use strict";i.r(e),i.d(e,{AreaCard:()=>r});var s=i(981);class r extends s.AbstractCard{#c={type:"custom:mushroom-template-card",primary:void 0,icon:"mdi:texture-box",icon_color:"blue",tap_action:{action:"navigate",navigation_path:void 0},hold_action:{action:"none"}};constructor(t,e={}){super(t),this.#c.primary=t.name,this.#c.tap_action.navigation_path=t.area_id??t.name,this.mergeOptions(this.#c,e),!e.primary&&e.name&&(this.options.primary=e.name)}}},917:(t,e,i)=>{"use strict";i.r(e),i.d(e,{BinarySensorCard:()=>r});var s=i(408);class r extends s.SensorCard{#c={type:"custom:mushroom-entity-card",icon:"mdi:power-cycle",icon_color:"green"};constructor(t,e={}){super(t),this.mergeOptions(this.#c,e)}}},497:(t,e,i)=>{"use strict";i.r(e),i.d(e,{CameraCard:()=>r});var s=i(981);class r extends s.AbstractCard{#c={type:"custom:webrtc-camera"};constructor(t,e={}){super(t),this.mergeOptions(this.#c,e)}}},898:(t,e,i)=>{"use strict";i.r(e),i.d(e,{ClimateCard:()=>r});var s=i(981);class r extends s.AbstractCard{#c={type:"custom:mushroom-climate-card",icon:void 0,hvac_modes:["off","cool","heat","fan_only"],show_temperature_control:!0};constructor(t,e={}){super(t),this.mergeOptions(this.#c,e)}}},499:(t,e,i)=>{"use strict";i.r(e),i.d(e,{CoverCard:()=>r});var s=i(981);class r extends s.AbstractCard{#c={type:"custom:mushroom-cover-card",icon:void 0,show_buttons_control:!0,show_position_control:!0,show_tilt_position_control:!0};constructor(t,e={}){super(t),this.mergeOptions(this.#c,e)}}},297:(t,e,i)=>{"use strict";i.r(e),i.d(e,{FanCard:()=>r});var s=i(981);class r extends s.AbstractCard{#c={type:"custom:mushroom-fan-card",icon:void 0,show_percentage_control:!0,show_oscillate_control:!0,icon_animation:!0};constructor(t,e={}){super(t),this.mergeOptions(this.#c,e)}}},698:(t,e,i)=>{"use strict";i.r(e),i.d(e,{LightCard:()=>r});var s=i(981);class r extends s.AbstractCard{#c={type:"custom:mushroom-light-card",icon:void 0,show_brightness_control:!0,show_color_control:!0,use_light_color:!0,double_tap_action:{target:{entity_id:void 0},action:"call-service",service:"light.turn_on",data:{rgb_color:[255,255,255]}}};constructor(t,e={}){super(t),this.mergeOptions(this.#c,e)}}},568:(t,e,i)=>{"use strict";i.r(e),i.d(e,{MediaPlayerCard:()=>r});var s=i(981);class r extends s.AbstractCard{#c={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"]};constructor(t,e={}){super(t),this.mergeOptions(this.#c,e)}}},190:(t,e,i)=>{"use strict";i.r(e),i.d(e,{MiscellaneousCard:()=>r});var s=i(981);class r extends s.AbstractCard{#c={type:"custom:mushroom-entity-card",icon_color:"blue-grey"};constructor(t,e={}){super(t),this.mergeOptions(this.#c,e)}}},543:(t,e,i)=>{"use strict";i.r(e),i.d(e,{PersonCard:()=>r});var s=i(981);class r extends s.AbstractCard{#c={type:"custom:mushroom-person-card",layout:"vertical",primary_info:"none",secondary_info:"none",icon_type:"entity-picture"};constructor(t,e={}){super(t),this.mergeOptions(this.#c,e)}}},408:(t,e,i)=>{"use strict";i.r(e),i.d(e,{SensorCard:()=>r});var s=i(981);class r extends s.AbstractCard{#c={type:"custom:mushroom-entity-card",icon:"mdi:information",animate:!0,line_color:"green"};constructor(t,e={}){super(t),this.mergeOptions(this.#c,e)}}},177:(t,e,i)=>{"use strict";i.r(e),i.d(e,{SwitchCard:()=>r});var s=i(981);class r extends s.AbstractCard{#c={type:"custom:mushroom-entity-card",icon:void 0,tap_action:{action:"toggle"}};constructor(t,e={}){super(t),this.mergeOptions(this.#c,e)}}},402:(t,e,i)=>{"use strict";i.r(e),i.d(e,{TitleCard:()=>s});class s{#d;#l={title:void 0,subtitle:void 0,showControls:!0,iconOn:"mdi:power-on",iconOff:"mdi:power-off",onService:"none",offService:"none"};constructor(t,e={}){this.#d=t.map((t=>t.area_id)).filter((t=>t)),this.#l={...this.#l,...e}}createCard(){const t=[{type:"custom:mushroom-title-card",title:this.#l.title,subtitle:this.#l.subtitle}];return this.#l.showControls&&t.push({type:"horizontal-stack",cards:[{type:"custom:mushroom-template-card",icon:this.#l.iconOff,layout:"vertical",icon_color:"red",tap_action:{action:"call-service",service:this.#l.offService,target:{area_id:this.#d},data:{}}},{type:"custom:mushroom-template-card",icon:this.#l.iconOn,layout:"vertical",icon_color:"amber",tap_action:{action:"call-service",service:this.#l.onService,target:{area_id:this.#d},data:{}}}]}),{type:"horizontal-stack",cards:t}}}},244:()=>{},175:(t,e,i)=>{var s={"./AbstractCard":[981,9],"./AbstractCard.js":[981,9],"./AreaCard":[138,9,179],"./AreaCard.js":[138,9,179],"./BinarySensorCard":[917,9,179],"./BinarySensorCard.js":[917,9,179],"./CameraCard":[497,9,179],"./CameraCard.js":[497,9,179],"./ClimateCard":[898,9,179],"./ClimateCard.js":[898,9,179],"./CoverCard":[499,9,179],"./CoverCard.js":[499,9,179],"./FanCard":[297,9,179],"./FanCard.js":[297,9,179],"./LightCard":[698,9,179],"./LightCard.js":[698,9,179],"./MediaPlayerCard":[568,9,179],"./MediaPlayerCard.js":[568,9,179],"./MiscellaneousCard":[190,9,179],"./MiscellaneousCard.js":[190,9,179],"./PersonCard":[543,9,179],"./PersonCard.js":[543,9,179],"./SensorCard":[408,9],"./SensorCard.js":[408,9],"./SwitchCard":[177,9,179],"./SwitchCard.js":[177,9,179],"./TitleCard":[402,9],"./TitleCard.js":[402,9],"./typedefs":[244,7,179],"./typedefs.js":[244,7,179]};function r(t){if(!i.o(s,t))return Promise.resolve().then((()=>{var e=new Error("Cannot find module '"+t+"'");throw e.code="MODULE_NOT_FOUND",e}));var e=s[t],r=e[0];return Promise.all(e.slice(2).map(i.e)).then((()=>i.t(r,16|e[1])))}r.keys=()=>Object.keys(s),r.id=175,t.exports=r},354:(t,e,i)=>{"use strict";i.r(e),i.d(e,{ClimateChip:()=>r});var s=i(84);class r{#d;#l={};constructor(t,e={}){if(!s.W.isInitialized())throw new Error("The Helper module must be initialized before using this one.");this.#d=t.filter((t=>t)),this.#l={...this.#l,...e}}getChip(){return{type:"template",icon:"mdi:thermostat",icon_color:"orange",content:s.W.getCountTemplate("climate","ne","off"),tap_action:{action:"navigate",navigation_path:"thermostats"}}}}},454:(t,e,i)=>{"use strict";i.r(e),i.d(e,{CoverChip:()=>r});var s=i(84);class r{#d;#l={};constructor(t,e={}){if(!s.W.isInitialized())throw new Error("The Helper module must be initialized before using this one.");this.#d=t.filter((t=>t)),this.#l={...this.#l,...e}}getChip(){return{type:"template",icon:"mdi:window-open",icon_color:"cyan",content:s.W.getCountTemplate("cover","eq","open"),tap_action:{action:"navigate",navigation_path:"covers"}}}}},955:(t,e,i)=>{"use strict";i.r(e),i.d(e,{FanChip:()=>r});var s=i(84);class r{#d;#l={};constructor(t,e={}){if(!s.W.isInitialized())throw new Error("The Helper module must be initialized before using this one.");this.#d=t.filter((t=>t)),this.#l={...this.#l,...e}}getChip(){return{type:"template",icon:"mdi:fan",icon_color:"green",content:s.W.getCountTemplate("fan","eq","on"),tap_action:{action:"call-service",service:"fan.turn_off",target:{area_id:this.#d},data:{}},hold_action:{action:"navigate",navigation_path:"fans"}}}}},980:(t,e,i)=>{"use strict";i.r(e),i.d(e,{LightChip:()=>r});var s=i(84);class r{#d;#l={};constructor(t,e={}){if(!s.W.isInitialized())throw new Error("The Helper module must be initialized before using this one.");this.#d=t.filter((t=>t)),this.#l={...this.#l,...e}}getChip(){return{type:"template",icon:"mdi:lightbulb-group",icon_color:"amber",content:s.W.getCountTemplate("light","eq","on"),tap_action:{action:"call-service",service:"light.turn_off",target:{area_id:this.#d},data:{}},hold_action:{action:"navigate",navigation_path:"lights"}}}}},25:(t,e,i)=>{"use strict";i.r(e),i.d(e,{SwitchChip:()=>r});var s=i(84);class r{#d;#l={};constructor(t,e={}){if(!s.W.isInitialized())throw new Error("The Helper module must be initialized before using this one.");this.#d=t.filter((t=>t)),this.#l={...this.#l,...e}}getChip(){return{type:"template",icon:"mdi:dip-switch",icon_color:"blue",content:s.W.getCountTemplate("switch","eq","on"),tap_action:{action:"call-service",service:"switch.turn_off",target:{area_id:this.#d},data:{}},hold_action:{action:"navigate",navigation_path:"switches"}}}}},369:(t,e,i)=>{"use strict";i.r(e),i.d(e,{WeatherChip:()=>s});class s{#h;#l={show_temperature:!0,show_conditions:!0};constructor(t,e={}){this.#h=t,this.#l={...this.#l,...e}}getChip(){return{type:"weather",entity:this.#h,...this.#l}}}},837:(t,e,i)=>{var s={"./ClimateChip":[354,179],"./ClimateChip.js":[354,179],"./CoverChip":[454,179],"./CoverChip.js":[454,179],"./FanChip":[955,179],"./FanChip.js":[955,179],"./LightChip":[980,179],"./LightChip.js":[980,179],"./SwitchChip":[25,179],"./SwitchChip.js":[25,179],"./WeatherChip":[369,179],"./WeatherChip.js":[369,179]};function r(t){if(!i.o(s,t))return Promise.resolve().then((()=>{var e=new Error("Cannot find module '"+t+"'");throw e.code="MODULE_NOT_FOUND",e}));var e=s[t],r=e[0];return i.e(e[1]).then((()=>i(r)))}r.keys=()=>Object.keys(s),r.id=837,t.exports=r},721:(t,e,i)=>{"use strict";i.r(e),i.d(e,{AbstractView:()=>o});var s=i(84),r=i(402);class o{options={title:null,path:null,icon:"mdi:view-dashboard",subview:!1};viewTitleCard;constructor(){if(this.constructor===o)throw new Error("Abstract classes can't be instantiated.");if(!s.W.isInitialized())throw new Error("The Helper module must be initialized before using this one.")}mergeOptions(t,e){this.options={...t,...e}}createViewCards(){const t=[this.viewTitleCard];for(const e of s.W.areas){const o=[],a=s.W.getDeviceEntities(e,this.domain),n=s.W.sanitizeClassName(this.domain+"Card");i(175)(`./${n}`).then((t=>{if(a.length){o.push(new r.TitleCard([e],{title:e.name,...this.options.titleCard}).createCard());for(const e of a){const i=(s.W.strategyOptions.entity_config??[]).find((t=>t.entity===e.entity_id))??new t[n](e).getCard();o.push(i)}}})),t.push({type:"vertical-stack",cards:o})}return t}async getView(){return{...this.options,cards:await this.createViewCards()}}}},458:(t,e,i)=>{"use strict";i.r(e),i.d(e,{CameraView:()=>a});var s=i(84),r=i(402),o=i(721);class a extends o.AbstractView{#p="camera";#c={title:"Cameras",path:"cameras",icon:"mdi:cctv",subview:!1,titleCard:{showControls:!1}};#u={title:"All Cameras",...this.options.titleCard};constructor(t={}){super(),this.mergeOptions(this.#c,t),this.viewTitleCard=new r.TitleCard(s.W.areas,{...this.#u,...this.options.titleCard}).createCard()}get domain(){return this.#p}}},310:(t,e,i)=>{"use strict";i.r(e),i.d(e,{ClimateView:()=>a});var s=i(84),r=i(402),o=i(721);class a extends o.AbstractView{#p="climate";#c={title:"Climates",path:"climates",icon:"mdi:thermostat",subview:!1,titleCard:{showControls:!1}};#u={title:"All Climates",subtitle:s.W.getCountTemplate(this.domain,"ne","off")+" climates on",...this.options.titleCard};constructor(t={}){super(),this.mergeOptions(this.#c,t),this.viewTitleCard=new r.TitleCard(s.W.areas,{...this.#u,...this.options.titleCard}).createCard()}get domain(){return this.#p}}},401:(t,e,i)=>{"use strict";i.r(e),i.d(e,{CoverView:()=>a});var s=i(84),r=i(402),o=i(721);class a extends o.AbstractView{#p="cover";#c={title:"Covers",path:"covers",icon:"mdi:window-open",subview:!1,titleCard:{iconOn:"mdi:arrow-up",iconOff:"mdi:arrow-down",onService:"cover.open_cover",offService:"cover.close_cover"}};#u={title:"All Covers",subtitle:s.W.getCountTemplate(this.domain,"eq","open")+" covers open"};constructor(t={}){super(),this.mergeOptions(this.#c,t),this.viewTitleCard=new r.TitleCard(s.W.areas,{...this.#u,...this.options.titleCard}).createCard()}get domain(){return this.#p}}},902:(t,e,i)=>{"use strict";i.r(e),i.d(e,{FanView:()=>a});var s=i(84),r=i(402),o=i(721);class a extends o.AbstractView{#p="fan";#c={title:"Fans",path:"fans",icon:"mdi:fan",subview:!1,titleCard:{iconOn:"mdi:fan",iconOff:"mdi:fan-off",onService:"fan.turn_on",offService:"fan.turn_off"}};#u={title:"All Fans",subtitle:s.W.getCountTemplate(this.domain,"eq","on")+" fans on"};constructor(t={}){super(),this.mergeOptions(this.#c,t),this.viewTitleCard=new r.TitleCard(s.W.areas,{...this.#u,...this.options.titleCard}).createCard()}get domain(){return this.#p}}},530:(t,e,i)=>{"use strict";i.r(e),i.d(e,{HomeView:()=>o});var s=i(84),r=i(721);class o extends r.AbstractView{#c={title:"Home",path:"home",subview:!1};constructor(t={}){super(),this.mergeOptions(this.#c,t)}async createViewCards(){return await Promise.all([this.#m(),this.#f(),this.#g()]).then((([t,e,i])=>{const r=s.W.strategyOptions,o=[{type:"custom:mushroom-chips-card",alignment:"center",chips:t},{type:"horizontal-stack",cards:e},{type:"custom:mushroom-template-card",primary:"{% set time = now().hour %} {% if (time >= 18) %} Good Evening, {{user}}! {% elif (time >= 12) %} Good Afternoon, {{user}}! {% elif (time >= 5) %} Good Morning, {{user}}! {% else %} Hello, {{user}}! {% endif %}",icon:"mdi:hand-wave",icon_color:"orange",tap_action:{action:"none"},double_tap_action:{action:"none"},hold_action:{action:"none"}}];return r.quick_access_cards&&o.push(...r.quick_access_cards),o.push({type:"custom:mushroom-title-card",title:"Areas"},{type:"vertical-stack",cards:i}),r.extra_cards&&o.push(...r.extra_cards),o}))}async#m(){const t=[],e=s.W.strategyOptions.chips,r=["light","fan","cover","switch","climate"],o=s.W.areas.map((t=>t.area_id));let a;const n=e?.weather_entity??s.W.entities.find((t=>t.entity_id.startsWith("weather.")&&null==t.disabled_by&&null==t.hidden_by)).entity_id;if(n)try{a=await Promise.resolve().then(i.bind(i,369));const e=new a.WeatherChip(n);t.push(e.getChip())}catch(t){console.error(s.W.debug?t:"An error occurred while creating the weather chip!")}for(let n of r)if(e?.[`${n}_count`]??1){const e=s.W.sanitizeClassName(n+"Chip");try{a=await i(837)(`./${e}`);const s=new a[e](o);t.push(s.getChip())}catch(t){console.error(s.W.debug?t:`An error occurred while creating the ${n} chip!`)}}return e?.extra_chips&&t.push(...e.extra_chips),t}#f(){const t=[];return Promise.resolve().then(i.bind(i,543)).then((e=>{for(const i of s.W.entities.filter((t=>t.entity_id.startsWith("person."))))t.push(new e.PersonCard(i).getCard())})),t}#g(){const t=[];return Promise.resolve().then(i.bind(i,138)).then((e=>{const i=[];for(const t of s.W.areas)s.W.strategyOptions.areas[t.area_id]?.hidden||i.push(new e.AreaCard(t,s.W.strategyOptions.areas[t.area_id??"undisclosed"]).getCard());for(let e=0;e{"use strict";i.r(e),i.d(e,{LightView:()=>a});var s=i(84),r=i(402),o=i(721);class a extends o.AbstractView{#p="light";#c={title:"Lights",path:"lights",icon:"mdi:lightbulb-group",subview:!1,titleCard:{iconOn:"mdi:lightbulb",iconOff:"mdi:lightbulb-off",onService:"light.turn_on",offService:"light.turn_off"}};#u={title:"All Lights",subtitle:s.W.getCountTemplate(this.domain,"eq","on")+" lights on"};constructor(t={}){super(),this.mergeOptions(this.#c,t),this.viewTitleCard=new r.TitleCard(s.W.areas,{...this.#u,...this.options.titleCard}).createCard()}get domain(){return this.#p}}},133:(t,e,i)=>{"use strict";i.r(e),i.d(e,{SwitchView:()=>a});var s=i(84),r=i(402),o=i(721);class a extends o.AbstractView{#p="switch";#c={title:"Switches",path:"switches",icon:"mdi:dip-switch",subview:!1,titleCard:{iconOn:"mdi:power-plug",iconOff:"mdi:power-plug-off",onService:"switch.turn_on",offService:"switch.turn_off"}};#u={title:"All Switches",subtitle:s.W.getCountTemplate(this.domain,"eq","on")+" switches on"};constructor(t={}){super(),this.mergeOptions(this.#c,t),this.viewTitleCard=new r.TitleCard(s.W.areas,{...this.#u,...this.options.titleCard}).createCard()}get domain(){return this.#p}}},654:(t,e,i)=>{"use strict";i.r(e)},968:(t,e,i)=>{var s={"./AbstractView":[721,179],"./AbstractView.js":[721,179],"./CameraView":[458,179],"./CameraView.js":[458,179],"./ClimateView":[310,179],"./ClimateView.js":[310,179],"./CoverView":[401,179],"./CoverView.js":[401,179],"./FanView":[902,179],"./FanView.js":[902,179],"./HomeView":[530,179],"./HomeView.js":[530,179],"./LightView":[587,179],"./LightView.js":[587,179],"./SwitchView":[133,179],"./SwitchView.js":[133,179],"./typedefs":[654,179],"./typedefs.js":[654,179]};function r(t){if(!i.o(s,t))return Promise.resolve().then((()=>{var e=new Error("Cannot find module '"+t+"'");throw e.code="MODULE_NOT_FOUND",e}));var e=s[t],r=e[0];return i.e(e[1]).then((()=>i(r)))}r.keys=()=>Object.keys(s),r.id=968,t.exports=r}},s={};function r(t){var e=s[t];if(void 0!==e)return e.exports;var o=s[t]={exports:{}};return i[t](o,o.exports,r),o.exports}e=Object.getPrototypeOf?t=>Object.getPrototypeOf(t):t=>t.__proto__,r.t=function(i,s){if(1&s&&(i=this(i)),8&s)return i;if("object"==typeof i&&i){if(4&s&&i.__esModule)return i;if(16&s&&"function"==typeof i.then)return i}var o=Object.create(null);r.r(o);var a={};t=t||[null,e({}),e([]),e(e)];for(var n=2&s&&i;"object"==typeof n&&!~t.indexOf(n);n=e(n))Object.getOwnPropertyNames(n).forEach((t=>a[t]=()=>i[t]));return a.default=()=>i,r.d(o,a),o},r.d=(t,e)=>{for(var i in e)r.o(e,i)&&!r.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,get:e[i]})},r.e=()=>Promise.resolve(),r.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},(()=>{"use strict";var t=r(84),e=r(408),i=r(402);customElements.define("ll-strategy-mushroom-strategy",class{static async generateDashboard(e){await t.W.initialize(e);const i=[];let s;for(let e of t.W.getExposedViewIds())try{const o=t.W.sanitizeClassName(e+"View");s=await r(968)(`./${o}`);const a=await new s[o](t.W.strategyOptions.views[e]).getView();i.push(a)}catch(i){console.error(t.W.debug?i:`View '${e}' couldn't be loaded!`)}for(let e of t.W.areas)e.hidden||i.push({title:e.name,path:e.area_id??e.name,subview:!0,strategy:{type:"custom:mushroom-strategy",options:{area:e,entity_config:t.W.strategyOptions.entity_config}}});return t.W.strategyOptions.extra_views&&i.push(...t.W.strategyOptions.extra_views),{views:i}}static async generateView(s){const o=t.W.getExposedDomainIds(),a=s.view.strategy.options.area,n=[...a.extra_cards??[]],c={entityConfig:s.view.strategy.options.entity_config};for(const s of o){if("default"===s)continue;const o=t.W.sanitizeClassName(s+"Card");let d=[];try{d=await r(175)(`./${o}`).then((r=>{let n=[];const d=t.W.getDeviceEntities(a,s);if(d.length){const l=new i.TitleCard([a],t.W.strategyOptions.domains[s]).createCard();if("sensor"===s){const i=t.W.getStateEntities(a,"sensor"),s=[];for(const t of d){let r=c.entityConfig?.find((e=>e.entity_id===t.entity_id));if(r){s.push(r);continue}const o=i.find((e=>e.entity_id===t.entity_id));let a={};o?.attributes.unit_of_measurement&&(a={type:"custom:mini-graph-card",entities:[t.entity_id]}),s.push(new e.SensorCard(t,a).getCard())}return n.push({type:"vertical-stack",cards:s}),n.unshift(l),n}for(const e of d){const i=(t.W.strategyOptions.entity_config??[]).find((t=>t.entity===e.entity_id))??new r[o](e).getCard();n.push(i)}if("binary_sensor"===s){const t=[];for(let e=0;et.area_id===a.area_id)).map((t=>t.id)),l=t.W.entities.filter((t=>(d.includes(t.device_id)||t.area_id===a.area_id)&&null==t.hidden_by&&null==t.disabled_by&&!o.includes(t.entity_id.split(".",1)[0])));if(l.length){let e=[];try{e=await Promise.resolve().then(r.bind(r,190)).then((e=>{const s=[new i.TitleCard([a],t.W.strategyOptions.domains.default).createCard()];for(const i of l){const r=(t.W.strategyOptions.entity_config??[]).find((t=>t.entity===i.entity_id))??new e.MiscellaneousCard(i).getCard();s.push(r)}return s}))}catch(e){console.error(t.W.debug?e:"An error occurred while creating the domain cards!")}n.push({type:"vertical-stack",cards:e})}return{cards:n}}})})()})(); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2fb649f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1335 @@ +{ + "name": "mushroom-strategy", + "version": "v1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mushroom-strategy", + "version": "v1.0.0", + "license": "MIT", + "devDependencies": { + "webpack": "^5", + "webpack-cli": "^5.0.2" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", + "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@types/eslint": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.37.0.tgz", + "integrity": "sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.15.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", + "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==", + "dev": true + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.5.tgz", + "integrity": "sha512-LHY/GSAZZRpsNQH+/oHqhRQ5FT7eoULcBqgfyTB5nQHogFnK3/7QoN7dLnwSE/JkUAF0SrRuclT7ODqMFtWxxQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.5", + "@webassemblyjs/helper-wasm-bytecode": "1.11.5" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.5.tgz", + "integrity": "sha512-1j1zTIC5EZOtCplMBG/IEwLtUojtwFVwdyVMbL/hwWqbzlQoJsWCOavrdnLkemwNoC/EOwtUFch3fuo+cbcXYQ==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.5.tgz", + "integrity": "sha512-L65bDPmfpY0+yFrsgz8b6LhXmbbs38OnwDCf6NpnMUYqa+ENfE5Dq9E42ny0qz/PdR0LJyq/T5YijPnU8AXEpA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.5.tgz", + "integrity": "sha512-fDKo1gstwFFSfacIeH5KfwzjykIE6ldh1iH9Y/8YkAZrhmu4TctqYjSh7t0K2VyDSXOZJ1MLhht/k9IvYGcIxg==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.5.tgz", + "integrity": "sha512-DhykHXM0ZABqfIGYNv93A5KKDw/+ywBFnuWybZZWcuzWHfbp21wUfRkbtz7dMGwGgT4iXjWuhRMA2Mzod6W4WA==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.5", + "@webassemblyjs/helper-api-error": "1.11.5", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.5.tgz", + "integrity": "sha512-oC4Qa0bNcqnjAowFn7MPCETQgDYytpsfvz4ujZz63Zu/a/v71HeCAAmZsgZ3YVKec3zSPYytG3/PrRCqbtcAvA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.5.tgz", + "integrity": "sha512-uEoThA1LN2NA+K3B9wDo3yKlBfVtC6rh0i4/6hvbz071E8gTNZD/pT0MsBf7MeD6KbApMSkaAK0XeKyOZC7CIA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.5", + "@webassemblyjs/helper-buffer": "1.11.5", + "@webassemblyjs/helper-wasm-bytecode": "1.11.5", + "@webassemblyjs/wasm-gen": "1.11.5" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.5.tgz", + "integrity": "sha512-37aGq6qVL8A8oPbPrSGMBcp38YZFXcHfiROflJn9jxSdSMMM5dS5P/9e2/TpaJuhE+wFrbukN2WI6Hw9MH5acg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.5.tgz", + "integrity": "sha512-ajqrRSXaTJoPW+xmkfYN6l8VIeNnR4vBOTQO9HzR7IygoCcKWkICbKFbVTNMjMgMREqXEr0+2M6zukzM47ZUfQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.5.tgz", + "integrity": "sha512-WiOhulHKTZU5UPlRl53gHR8OxdGsSOxqfpqWeA2FmcwBMaoEdz6b2x2si3IwC9/fSPLfe8pBMRTHVMk5nlwnFQ==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.5.tgz", + "integrity": "sha512-C0p9D2fAu3Twwqvygvf42iGCQ4av8MFBLiTb+08SZ4cEdwzWx9QeAHDo1E2k+9s/0w1DM40oflJOpkZ8jW4HCQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.5", + "@webassemblyjs/helper-buffer": "1.11.5", + "@webassemblyjs/helper-wasm-bytecode": "1.11.5", + "@webassemblyjs/helper-wasm-section": "1.11.5", + "@webassemblyjs/wasm-gen": "1.11.5", + "@webassemblyjs/wasm-opt": "1.11.5", + "@webassemblyjs/wasm-parser": "1.11.5", + "@webassemblyjs/wast-printer": "1.11.5" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.5.tgz", + "integrity": "sha512-14vteRlRjxLK9eSyYFvw1K8Vv+iPdZU0Aebk3j6oB8TQiQYuO6hj9s4d7qf6f2HJr2khzvNldAFG13CgdkAIfA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.5", + "@webassemblyjs/helper-wasm-bytecode": "1.11.5", + "@webassemblyjs/ieee754": "1.11.5", + "@webassemblyjs/leb128": "1.11.5", + "@webassemblyjs/utf8": "1.11.5" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.5.tgz", + "integrity": "sha512-tcKwlIXstBQgbKy1MlbDMlXaxpucn42eb17H29rawYLxm5+MsEmgPzeCP8B1Cl69hCice8LeKgZpRUAPtqYPgw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.5", + "@webassemblyjs/helper-buffer": "1.11.5", + "@webassemblyjs/wasm-gen": "1.11.5", + "@webassemblyjs/wasm-parser": "1.11.5" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.5.tgz", + "integrity": "sha512-SVXUIwsLQlc8srSD7jejsfTU83g7pIGr2YYNb9oHdtldSxaOhvA5xwvIiWIfcX8PlSakgqMXsLpLfbbJ4cBYew==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.5", + "@webassemblyjs/helper-api-error": "1.11.5", + "@webassemblyjs/helper-wasm-bytecode": "1.11.5", + "@webassemblyjs/ieee754": "1.11.5", + "@webassemblyjs/leb128": "1.11.5", + "@webassemblyjs/utf8": "1.11.5" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.5.tgz", + "integrity": "sha512-f7Pq3wvg3GSPUPzR0F6bmI89Hdb+u9WXrSKc4v+N0aV0q6r42WoF92Jp2jEorBEBRoRNXgjp53nBniDXcqZYPA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.5", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.0.1.tgz", + "integrity": "sha512-njsdJXJSiS2iNbQVS0eT8A/KPnmyH4pv1APj2K0d1wrZcBLw+yppxOy4CGqa0OxDJkzfL/XELDhD8rocnIwB5A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.1.tgz", + "integrity": "sha512-fE1UEWTwsAxRhrJNikE7v4EotYflkEhBL7EbajfkPlf6E37/2QshOy/D48Mw8G5XMFlQtS6YV42vtbG9zBpIQA==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.2.tgz", + "integrity": "sha512-S9h3GmOmzUseyeFW3tYNnWS7gNUuwxZ3mmMq0JyW78Vx1SGKPSkt5bT4pB0rUnVfHjP0EL9gW2bOzmtiTfQt0A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001481", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz", + "integrity": "sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.369", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.369.tgz", + "integrity": "sha512-LfxbHXdA/S+qyoTEA4EbhxGjrxx7WK2h6yb5K2v0UCOufUKX+VZaHbl3svlzZfv9sGseym/g3Ne4DpsgRULmqg==", + "dev": true + }, + "node_modules/enhanced-resolve": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.13.0.tgz", + "integrity": "sha512-eyV8f0y1+bzyfh8xAwW/WTSZpLbjhqc4ne9eGSH4Zo2ejdyiNG9pU6mf9DG8a7+Auk6MFTlNOT4Y2y/9k8GKVg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", + "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-core-module": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", + "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "dev": true + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/schema-utils": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz", + "integrity": "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.1.tgz", + "integrity": "sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz", + "integrity": "sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.5" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.80.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.80.0.tgz", + "integrity": "sha512-OIMiq37XK1rWO8mH9ssfFKZsXg4n6klTEDL7S8/HqbAOBBaiy8ABvXvz0dDCXeEF9gqwxSvVk611zFPjS8hJxA==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.13.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.0.2.tgz", + "integrity": "sha512-4y3W5Dawri5+8dXm3+diW6Mn1Ya+Dei6eEVAdIduAmYNLzv1koKVAqsfgrrc9P2mhrYHQphx5htnGkcNwtubyQ==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.0.1", + "@webpack-cli/info": "^2.0.1", + "@webpack-cli/serve": "^2.0.2", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..aff6a72 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "mushroom-strategy", + "version": "v1.0.0", + "description": "Automatically create a dashboard using Mushroom cards", + "keywords": [ + "strategy", + "mushroom" + ], + "homepage": "https://github.com/AalianKhan/mushroom-strategy", + "bugs": "https://github.com/AalianKhan/mushroom-strategy/issues", + "license": "MIT", + "author": { + "name": "Aalian Khan" + }, + "contributors": [ + { + "name": "Ferry Cools" + } + ], + "repository": { + "type": "git", + "url": "https://github.com/AalianKhan/mushroom-strategy" + }, + "devDependencies": { + "webpack": "^5", + "webpack-cli": "^5.0.2" + }, + "scripts": { + "build": "webpack", + "build-dev": "webpack --config webpack.dev.config.js" + } +} diff --git a/src/Helper.js b/src/Helper.js new file mode 100644 index 0000000..dd87fe6 --- /dev/null +++ b/src/Helper.js @@ -0,0 +1,447 @@ +import {optionDefaults} from "./optionDefaults"; + +/** + * Helper Class + * + * Contains the objects of Home Assistant's registries and helper methods. + */ +class Helper { + /** + * An array of entities from Home Assistant's entity registry. + * + * @type {hassEntity[]} + * @private + */ + static #entities; + /** + * An array of entities from Home Assistant's device registry. + * + * @type {deviceEntity[]} + * @private + */ + static #devices; + /** + * An array of entities from Home Assistant's area registry. + * + * @type {areaEntity[]} + * @private + */ + static #areas = []; + /** + * An array of state entities from Home Assistant's Hass object. + * + * @type {hassObject["states"]} + * @private + */ + static #hassStates; + + /** + * Indicates whether this module is initialized. + * + * @type {boolean} True if initialized. + * @private + */ + static #initialized = false; + + /** + * The Custom strategy configuration. + * + * @type {customStrategyOptions | {}} + * @private + */ + static #strategyOptions = {}; + + /** + * Set to true for more verbose information in the console. + * + * @type {boolean} + */ + static debug = optionDefaults.debug; + + /** + * Class constructor. + * + * This class shouldn't be instantiated directly. Instead, it should be initialized with method initialize(). + * @throws {Error} If trying to instantiate this class. + */ + constructor() { + throw new Error("This class should be invoked with method initialize() instead of using the keyword new!"); + } + + /** + * Custom strategy configuration. + * + * @returns {customStrategyOptions|{}} + * @static + */ + static get strategyOptions() { + return this.#strategyOptions; + } + + /** + * @returns {areaEntity[]} + * @static + */ + static get areas() { + return this.#areas; + } + + /** + * @returns {deviceEntity[]} + * @static + */ + static get devices() { + return this.#devices; + } + + /** + * @returns {hassEntity[]} + * @static + */ + static get entities() { + return this.#entities; + } + + /** + * @returns {boolean} + * @static + */ + static get debug() { + return this.debug; + } + + /** + * Initialize this module. + * + * @param {dashBoardInfo | viewInfo} info Strategy information object. + * @returns {Promise} + * @static + */ + static async initialize(info) { + this.#hassStates = info.hass.states; + + try { + // Query the registries of Home Assistant. + [this.#entities, this.#devices, this.#areas] = await Promise.all([ + info.hass.callWS({type: "config/entity_registry/list"}), + info.hass.callWS({type: "config/device_registry/list"}), + info.hass.callWS({type: "config/area_registry/list"}), + ]); + } catch (e) { + console.error(Helper.debug ? e : "An error occurred while querying Home assistant's registries!"); + } + + // Cloning is required for the purpose of the required undisclosed area. + this.#strategyOptions = structuredClone(info.config.strategy.options || {}); + this.debug = this.#strategyOptions.debug; + + // Setup required configuration entries. + // TODO: Refactor to something smarter than repeating code for areas, views and domains. + this.#strategyOptions.areas = this.#strategyOptions.areas ?? {}; + this.#strategyOptions.views = this.#strategyOptions.views ?? {}; + this.#strategyOptions.domains = this.#strategyOptions.domains ?? {}; + + // Setup and add the undisclosed area if not hidden in the strategy options. + if (!this.#strategyOptions.areas.undisclosed?.hidden) { + this.#strategyOptions.areas.undisclosed = { + ...optionDefaults.areas.undisclosed, + ...this.#strategyOptions.areas.undisclosed, + }; + + // Make sure the area_id of the custom undisclosed area remains null. + this.#strategyOptions.areas.undisclosed.area_id = null; + + this.#areas.push(this.#strategyOptions.areas.undisclosed); + } + + // Merge custom areas of the strategy options into hass areas. + this.#areas = Helper.areas.map(area => { + return {...area, ...this.#strategyOptions.areas[area.area_id ?? "undisclosed"]}; + }); + + // Sort hass areas by order first and then by name. + this.#areas.sort((a, b) => { + return (a.order ?? Infinity) - (b.order ?? Infinity) || a.name.localeCompare(b.name); + }); + + // Merge the views of the strategy options and the default views. + for (const view of Object.keys(optionDefaults.views)) { + this.#strategyOptions.views[view] = { + ...optionDefaults.views[view], + ...(this.#strategyOptions.views[view]), + }; + } + + // Sort 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?.localeCompare(b.title); + }), + ); + + // Merge the domains of the strategy options and the default domains. + for (const domain of Object.keys(optionDefaults.domains)) { + this.#strategyOptions.domains[domain] = { + ...optionDefaults.domains[domain], + ...(this.#strategyOptions.domains[domain]), + }; + } + + // Sort 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?.localeCompare(b.title); + }), + ); + + this.#initialized = true; + } + + /** + * Get the initialization status of the Helper class. + * + * @returns {boolean} True if this module is initialized. + * @static + */ + static isInitialized() { + return this.#initialized; + } + + /** + * Get a template string to define the number of a given domain's entities with a certain state. + * + * States are compared against a given value by a given operator. + * + * @param {string} domain The domain of the entities. + * @param {string} operator The Comparison operator between state and value. + * @param {string} value The value to which the state is compared against. + * + * @return {string} The template string. + * @static + */ + static getCountTemplate(domain, operator, value) { + // noinspection JSMismatchedCollectionQueryUpdate (False positive per 17-04-2023) + /** + * Array of entity state-entries, filtered by domain. + * + * Each element contains a template-string which is used to access home assistant's state machine (state object) in + * a template. + * E.g. "states['light.kitchen']" + * + * The array excludes hidden and disabled entities. + * + * @type {string[]} + */ + const states = []; + + if (!this.isInitialized()) { + console.warn("Helper class should be initialized before calling this method!"); + } + + // Get the ID of the devices which are linked to the given area. + for (const area of this.#areas) { + const areaDeviceIds = this.#devices.filter(device => { + return device.area_id === area.area_id; + }).map(device => { + return device.id; + }); + + // Get the entities of which all conditions of the callback function are met. @see areaFilterCallback. + const newStates = this.#entities.filter( + this.#areaFilterCallback, { + area: area, + domain: domain, + areaDeviceIds: areaDeviceIds, + }) + .map(entity => `states['${entity.entity_id}']`); + + states.push(...newStates); + } + + return `{% set entities = [${states}] %} {{ entities | selectattr('state','${operator}','${value}') | list | count }}`; + } + + /** + * Callback function for filtering entities. + * + * Entities of which all the conditions below are met are kept: + * 1. Or/Neither the entity's linked device (if any) or/nor the entity itself is lined to the given area. + * (See variable areaMatch) + * 2. The entity's domain matches the given domain. + * 3. The entity is not hidden and is not disabled. + * + * @param {hassEntity} entity The current hass entity to evaluate. + * @this {areaFilterContext} + * + * @return {boolean} True to keep the entity. + * @static + */ + static #areaFilterCallback(entity) { + const areaMatch = this.area.area_id + // Area is a hass entity; The entity's linked device or the entity itself is linked to the given area. + ? this.areaDeviceIds.includes(entity.device_id) || entity.area_id === this.area.area_id + // Undisclosed area; Neither the entity's linked device (if any), nor the entity itself is linked to any area. + : (this.areaDeviceIds.includes(entity.device_id) || !entity.device_id) && !entity.area_id; + + return ( + areaMatch + && entity.entity_id.startsWith(`${this.domain}.`) + && entity.hidden_by == null && entity.disabled_by == null + ); + } + + /** + * Get device entities from the entity registry, filtered by area and domain. + * + * The entity registry is a registry where Home-Assistant keeps track of all entities. + * A device is represented in Home Assistant via one or more entities. + * + * The result excludes hidden and disabled entities. + * + * @param {areaEntity} area Area entity. + * @param {string} domain The domain of the entity-id. + * + * @return {hassEntity[]} Array of device entities. + * @static + */ + static getDeviceEntities(area, domain) { + if (!this.isInitialized()) { + console.warn("Helper class should be initialized before calling this method!"); + } + + // Get the ID of the devices which are linked to the given area. + const areaDeviceIds = this.#devices.filter(device => { + return device.area_id === area.area_id; + }).map(device => { + + return device.id; + }); + + // Return the entities of which all conditions of the callback function are met. @see areaFilterCallback. + return this.#entities.filter( + this.#areaFilterCallback, { + area: area, + domain: domain, + areaDeviceIds: areaDeviceIds, + }) + .sort((a, b) => { + /** @type hassEntity */ + return a.original_name?.localeCompare(b.original_name); + }); + } + + /** + * Get state entities, filtered by area and domain. + * + * The result excludes hidden and disabled entities. + * + * @param {areaEntity} area Area entity. + * @param {string} domain Domain of the entity-id. + * + * @return {stateObject[]} Array of state entities. + */ + static getStateEntities(area, domain) { + if (!this.isInitialized()) { + console.warn("Helper class should be initialized before calling this method!"); + } + + const states = []; + + // Create a map for the hassEntities and devices {id: object} to improve lookup speed. + /** @type {Object} */ + const entityMap = Object.fromEntries(this.#entities.map(entity => [entity.entity_id, entity])); + /** @type {Object} */ + const deviceMap = Object.fromEntries(this.#devices.map(device => [device.id, device])); + + // Get states whose entity-id starts with the given string. + const stateEntities = Object.values(this.#hassStates).filter( + state => state.entity_id.startsWith(`${domain}.`), + ); + + for (const state of stateEntities) { + const hassEntity = entityMap[state.entity_id]; + const device = deviceMap[hassEntity?.device_id]; + + // Collect states of which any (whichever comes first) of the conditions below are met: + // 1. The linked entity is linked to the given area. + // 2. The entity is linked to a device, and the linked device is linked to the given area. + if ( + (hassEntity?.area_id === area.area_id) + || (device && device.area_id === area.area_id) + ) { + states.push(state); + } + } + + return states; + } + + /** + * Sanitize a classname. + * + * The name is sanitized by capitalizing the first character of the name or after an underscore. + * Underscores are removed. + * + * @param {string} className Name of the class to sanitize. + * @returns {string} The sanitized classname. + */ + static sanitizeClassName(className) { + className = className.charAt(0).toUpperCase() + className.slice(1); + + return className.replace(/([-_][a-z])/g, group => + group + .toUpperCase() + .replace("-", "") + .replace("_", ""), + ); + } + + /** + * Get the keys of nested objects by its property value. + * + * @param {Object} object An object of objects. + * @param {string|number} property The name of the property to evaluate. + * @param {*} value The value which the property should match. + * + * @return {string[]|number[]} An array with keys. + */ + static #getObjectKeysByPropertyValue(object, property, value) { + const keys = []; + + for (const key of Object.keys(object)) { + if (object[key][property] === value) { + keys.push(key); + } + } + + return keys; + } + + /** + * Get the ids of the views which aren't set to hidden in the strategy options. + * + * @return {string[]} An array of view ids. + */ + static getExposedViewIds() { + if (!this.isInitialized()) { + console.warn("Helper class should be initialized before calling this method!"); + } + + return this.#getObjectKeysByPropertyValue(this.#strategyOptions.views, "hidden", false); + } + + /** + * Get the ids of the domain ids which aren't set to hidden in the strategy options. + * + * @return {string[]} An array of domain ids. + */ + static getExposedDomainIds() { + if (!this.isInitialized()) { + console.warn("Helper class should be initialized before calling this method!"); + } + + return this.#getObjectKeysByPropertyValue(this.#strategyOptions.domains, "hidden", false); + } +} + +export {Helper}; diff --git a/src/cards/AbstractCard.js b/src/cards/AbstractCard.js new file mode 100644 index 0000000..6aeea91 --- /dev/null +++ b/src/cards/AbstractCard.js @@ -0,0 +1,81 @@ +import {Helper} from "../Helper"; + +/** + * Abstract Card Class + * + * To create a new card, extend the new class with this one. + * + * @class + * @abstract + */ +class AbstractCard { + /** + * Entity to create the card for. + * + * @type {hassEntity | areaEntity} + */ + entity; + + /** + * Options for creating a card. + * + * @type {abstractOptions} + */ + options = { + type: "custom:mushroom-entity-card", + icon: "mdi:help-circle", + double_tap_action: { + action: null, + }, + }; + + /** + * Class constructor. + * + * @param {hassEntity | areaEntity} entity The hass entity to create a card for. + * @throws {Error} If the Helper module isn't initialized. + */ + constructor(entity) { + if (this.constructor === AbstractCard) { + throw new Error("Abstract classes can't be instantiated."); + } + + if (!Helper.isInitialized()) { + throw new Error("The Helper module must be initialized before using this one."); + } + + this.entity = entity; + } + + /** + * Merge the default options of this class and the custom options into the options of the parent class. + * + * @param {Object} [defaultOptions={}] Default options for the card. + * @param {Object} [customOptions={}] Custom Options for the card. + */ + mergeOptions(defaultOptions, customOptions) { + this.options = { + ...this.options, + ...defaultOptions, + ...customOptions, + }; + + try { + this.options.double_tap_action.target.entity_id = this.entity.entity_id; + } catch { } + } + + /** + * Get a card for an entity. + * + * @return {abstractOptions & Object} A card object. + */ + getCard() { + return { + entity: this.entity.entity_id, + ...this.options, + }; + } +} + +export {AbstractCard}; diff --git a/src/cards/AreaCard.js b/src/cards/AreaCard.js new file mode 100644 index 0000000..3b589d2 --- /dev/null +++ b/src/cards/AreaCard.js @@ -0,0 +1,56 @@ +import {AbstractCard} from "./AbstractCard"; + +/** + * Area Card Class + * + * Used to create a card for an entity of the area domain. + * + * @class + * @extends AbstractCard + */ +class AreaCard extends AbstractCard { + /** + * Default options of the card. + * + * @type {areaCardOptions} + * @private + */ + #defaultOptions = { + type: "custom:mushroom-template-card", + primary: undefined, + icon: "mdi:texture-box", + icon_color: "blue", + tap_action: { + action: "navigate", + navigation_path: undefined, + }, + hold_action: { + action: "none", + } + }; + + /** + * Class constructor. + * + * @param {areaEntity} area The area entity to create a card for. + * @param {areaCardOptions} [options={}] Options for the card. + * @throws {Error} If the Helper module isn't initialized. + */ + constructor(area, options = {}) { + super(area); + this.#defaultOptions.primary = area.name; + this.#defaultOptions.tap_action.navigation_path = area.area_id ?? area.name; + + this.mergeOptions( + this.#defaultOptions, + options, + ); + + // Override the area's name with a custom name, unless a custom primary text is set. + if (!options.primary && options.name) { + this.options.primary = options.name; + } + } +} + +export {AreaCard}; diff --git a/src/cards/BinarySensorCard.js b/src/cards/BinarySensorCard.js new file mode 100644 index 0000000..bd2cfdd --- /dev/null +++ b/src/cards/BinarySensorCard.js @@ -0,0 +1,41 @@ +import {SensorCard} from "./SensorCard"; + +/** + * Sensor Card Class + * + * Used to create a card for controlling an entity of the binary_sensor domain. + * + * @class + * @extends SensorCard + */ +class BinarySensorCard extends SensorCard { + /** + * Default options of the card. + * + * @type {sensorCardOptions} + * @private + */ + #defaultOptions = { + type: "custom:mushroom-entity-card", + icon: "mdi:power-cycle", + icon_color: "green", + }; + + /** + * Class constructor. + * + * @param {hassEntity} entity The hass entity to create a card for. + * @param {sensorCardOptions} [options={}] Options for the card. + * @throws {Error} If the Helper module isn't initialized. + */ + constructor(entity, options = {}) { + super(entity); + + this.mergeOptions( + this.#defaultOptions, + options, + ); + } +} + +export {BinarySensorCard}; diff --git a/src/cards/CameraCard.js b/src/cards/CameraCard.js new file mode 100644 index 0000000..58d5e59 --- /dev/null +++ b/src/cards/CameraCard.js @@ -0,0 +1,38 @@ +import {AbstractCard} from "./AbstractCard"; + +/** + * Camera Card Class + * + * Used to create a card for controlling an entity of the camera domain. + * + * @class + * @extends AbstractCard + */ +class CameraCard extends AbstractCard { + /** + * Default options of the card. + * + * @type {cameraCardOptions} + * @private + */ + #defaultOptions = { + type: "custom:webrtc-camera", + }; + + /** + * Class constructor. + * + * @param {hassEntity} entity The hass entity to create a card for. + * @param {cameraCardOptions} [options={}] Options for the card. + * @throws {Error} If the Helper module isn't initialized. + */ + constructor(entity, options = {}) { + super(entity); + this.mergeOptions( + this.#defaultOptions, + options, + ); + } +} + +export {CameraCard}; diff --git a/src/cards/ClimateCard.js b/src/cards/ClimateCard.js new file mode 100644 index 0000000..d7573df --- /dev/null +++ b/src/cards/ClimateCard.js @@ -0,0 +1,46 @@ +import {AbstractCard} from "./AbstractCard"; + +/** + * Climate Card Class + * + * Used to create a card for controlling an entity of the climate domain. + * + * @class + * @extends AbstractCard + */ +class ClimateCard extends AbstractCard { + /** + * Default options of the card. + * + * @type {climateCardOptions} + * @private + */ + #defaultOptions = { + type: "custom:mushroom-climate-card", + icon: undefined, + hvac_modes: [ + "off", + "cool", + "heat", + "fan_only", + ], + show_temperature_control: true, + }; + + /** + * Class constructor. + * + * @param {hassEntity} entity The hass entity to create a card for. + * @param {climateCardOptions} [options={}] Options for the card. + * @throws {Error} If the Helper module isn't initialized. + */ + constructor(entity, options = {}) { + super(entity); + this.mergeOptions( + this.#defaultOptions, + options, + ); + } +} + +export {ClimateCard}; diff --git a/src/cards/CoverCard.js b/src/cards/CoverCard.js new file mode 100644 index 0000000..e4c53d6 --- /dev/null +++ b/src/cards/CoverCard.js @@ -0,0 +1,42 @@ +import {AbstractCard} from "./AbstractCard"; + +/** + * Cover Card Class + * + * Used to create a card for controlling an entity of the cover domain. + * + * @class + * @extends AbstractCard + */ +class CoverCard extends AbstractCard { + /** + * Default options of the card. + * + * @type {coverCardOptions} + * @private + */ + #defaultOptions = { + type: "custom:mushroom-cover-card", + icon: undefined, + show_buttons_control: true, + show_position_control: true, + show_tilt_position_control: true, + }; + + /** + * Class constructor. + * + * @param {hassEntity} entity The hass entity to create a card for. + * @param {coverCardOptions} [options={}] Options for the card. + * @throws {Error} If the Helper module isn't initialized. + */ + constructor(entity, options = {}) { + super(entity); + this.mergeOptions( + this.#defaultOptions, + options, + ); + } +} + +export {CoverCard}; diff --git a/src/cards/FanCard.js b/src/cards/FanCard.js new file mode 100644 index 0000000..3fe2806 --- /dev/null +++ b/src/cards/FanCard.js @@ -0,0 +1,42 @@ +import {AbstractCard} from "./AbstractCard"; + +/** + * Fan Card Class + * + * Used to create a card for controlling an entity of the fan domain. + * + * @class + * @extends AbstractCard + */ +class FanCard extends AbstractCard { + /** + * Default options of the card. + * + * @type {fanCardOptions} + * @private + */ + #defaultOptions = { + type: "custom:mushroom-fan-card", + icon: undefined, + show_percentage_control: true, + show_oscillate_control: true, + icon_animation: true, + }; + + /** + * Class constructor. + * + * @param {hassEntity} entity The hass entity to create a card for. + * @param {fanCardOptions} [options={}] Options for the card. + * @throws {Error} If the Helper module isn't initialized. + */ + constructor(entity, options = {}) { + super(entity); + this.mergeOptions( + this.#defaultOptions, + options, + ); + } +} + +export {FanCard}; diff --git a/src/cards/LightCard.js b/src/cards/LightCard.js new file mode 100644 index 0000000..3278b0a --- /dev/null +++ b/src/cards/LightCard.js @@ -0,0 +1,52 @@ +import {AbstractCard} from "./AbstractCard"; + +/** + * Light Card Class + * + * Used to create a card for controlling an entity of the light domain. + * + * @class + * @extends AbstractCard + */ +class LightCard extends AbstractCard { + /** + * Default options of the card. + * + * @type {lightCardOptions} + * @private + */ + #defaultOptions = { + type: "custom:mushroom-light-card", + icon: undefined, + show_brightness_control: true, + show_color_control: true, + use_light_color: true, + double_tap_action: { + target: { + entity_id: undefined, + }, + action: "call-service", + service: "light.turn_on", + data: { + rgb_color: [255, 255, 255], + }, + }, + }; + + /** + * Class constructor. + * + * @param {hassEntity} entity The hass entity to create a card for. + * @param {lightCardOptions} [options={}] Options for the card. + * @throws {Error} If the Helper module isn't initialized. + */ + constructor(entity, options = {}) { + super(entity); + this.mergeOptions( + this.#defaultOptions, + options, + ); + } +} + +export {LightCard}; diff --git a/src/cards/MediaPlayerCard.js b/src/cards/MediaPlayerCard.js new file mode 100644 index 0000000..8e29f35 --- /dev/null +++ b/src/cards/MediaPlayerCard.js @@ -0,0 +1,49 @@ +import {AbstractCard} from "./AbstractCard"; + +/** + * Mediaplayer Card Class + * + * Used to create a card for controlling an entity of the media_player domain. + * + * @class + * @extends AbstractCard + */ +class MediaPlayerCard extends AbstractCard { + /** + * Default options of the card. + * + * @type {mediaPlayerCardOptions} + * @private + */ + #defaultOptions = { + type: "custom:mushroom-media-player-card", + use_media_info: true, + media_controls: [ + "on_off", + "play_pause_stop", + ], + show_volume_level: true, + volume_controls: [ + "volume_mute", + "volume_set", + "volume_buttons", + ], + }; + + /** + * Class constructor. + * + * @param {hassEntity} entity The hass entity to create a card for. + * @param {mediaPlayerCardOptions} [options={}] Options for the card. + * @throws {Error} If the Helper module isn't initialized. + */ + constructor(entity, options = {}) { + super(entity); + this.mergeOptions( + this.#defaultOptions, + options, + ); + } +} + +export {MediaPlayerCard}; diff --git a/src/cards/MiscellaneousCard.js b/src/cards/MiscellaneousCard.js new file mode 100644 index 0000000..d657855 --- /dev/null +++ b/src/cards/MiscellaneousCard.js @@ -0,0 +1,39 @@ +import {AbstractCard} from "./AbstractCard"; + +/** + * Miscellaneous Card Class + * + * Used to create a card an entity of any domain. + * + * @class + * @extends AbstractCard + */ +class MiscellaneousCard extends AbstractCard { + /** + * Default options of the card. + * + * @type {miscellaneousCardOptions} + * @private + */ + #defaultOptions = { + type: "custom:mushroom-entity-card", + icon_color: "blue-grey", + }; + + /** + * Class constructor. + * + * @param {hassEntity} entity The hass entity to create a card for. + * @param {miscellaneousCardOptions} [options={}] Options for the card. + * @throws {Error} If the Helper module isn't initialized. + */ + constructor(entity, options = {}) { + super(entity); + this.mergeOptions( + this.#defaultOptions, + options, + ); + } +} + +export {MiscellaneousCard}; diff --git a/src/cards/PersonCard.js b/src/cards/PersonCard.js new file mode 100644 index 0000000..db3bae6 --- /dev/null +++ b/src/cards/PersonCard.js @@ -0,0 +1,42 @@ +import {AbstractCard} from "./AbstractCard"; + +/** + * Person Card Class + * + * Used to create a card for an entity of the person domain. + * + * @class + * @extends AbstractCard + */ +class PersonCard extends AbstractCard { + /** + * Default options of the card. + * + * @type {personCardOptions} + * @private + */ + #defaultOptions = { + type: "custom:mushroom-person-card", + layout: "vertical", + primary_info: "none", + secondary_info: "none", + icon_type: "entity-picture", + }; + + /** + * Class constructor. + * + * @param {hassEntity} entity The hass entity to create a card for. + * @param {personCardOptions} [options={}] Options for the card. + * @throws {Error} If the Helper module isn't initialized. + */ + constructor(entity, options = {}) { + super(entity); + this.mergeOptions( + this.#defaultOptions, + options, + ); + } +} + +export {PersonCard}; diff --git a/src/cards/SensorCard.js b/src/cards/SensorCard.js new file mode 100644 index 0000000..ef8de22 --- /dev/null +++ b/src/cards/SensorCard.js @@ -0,0 +1,42 @@ +import {AbstractCard} from "./AbstractCard"; + +/** + * Sensor Card Class + * + * Used to create a card for controlling an entity of the sensor domain. + * + * @class + * @extends AbstractCard + */ +class SensorCard extends AbstractCard { + /** + * Default options of the card. + * + * @type {sensorCardOptions} + * @private + */ + #defaultOptions = { + type: "custom:mushroom-entity-card", + icon: "mdi:information", + animate: true, + line_color: "green", + }; + + /** + * Class constructor. + * + * @param {hassEntity} entity The hass entity to create a card for. + * @param {sensorCardOptions} [options={}] Options for the card. + * @throws {Error} If the Helper module isn't initialized. + */ + constructor(entity, options = {}) { + super(entity); + + this.mergeOptions( + this.#defaultOptions, + options, + ); + } +} + +export {SensorCard}; diff --git a/src/cards/SwitchCard.js b/src/cards/SwitchCard.js new file mode 100644 index 0000000..1701866 --- /dev/null +++ b/src/cards/SwitchCard.js @@ -0,0 +1,42 @@ +import {AbstractCard} from "./AbstractCard"; + +/** + * Switch Card Class + * + * Used to create a card for controlling an entity of the switch domain. + * + * @class + * @extends AbstractCard + */ +class SwitchCard extends AbstractCard { + /** + * Default options of the card. + * + * @type {switchCardOptions} + * @private + */ + #defaultOptions = { + type: "custom:mushroom-entity-card", + icon: undefined, + tap_action: { + action: "toggle", + }, + }; + + /** + * Class constructor. + * + * @param {hassEntity} entity The hass entity to create a card for. + * @param {switchCardOptions} [options={}] Options for the card. + * @throws {Error} If the Helper module isn't initialized. + */ + constructor(entity, options = {}) { + super(entity); + this.mergeOptions( + this.#defaultOptions, + options, + ); + } +} + +export {SwitchCard}; diff --git a/src/cards/TitleCard.js b/src/cards/TitleCard.js new file mode 100644 index 0000000..93a5e2d --- /dev/null +++ b/src/cards/TitleCard.js @@ -0,0 +1,101 @@ +/** + * Title Card class. + * + * Used for creating a Title Card. + * + * @class + */ +class TitleCard { + /** + * @type {string[]} An array of area ids. + * @private + */ + #areaIds; + + /** + * @type {titleCardOptions} + * @private + */ + #options = { + title: undefined, + subtitle: undefined, + showControls: true, + iconOn: "mdi:power-on", + iconOff: "mdi:power-off", + onService: "none", + offService: "none", + }; + + /** + * Class constructor. + * + * @param {areaEntity[]} areas An array of area entities. + * @param {titleCardOptions} options Title Card options. + */ + constructor(areas, options = {}) { + this.#areaIds = areas.map(area => area.area_id).filter(area_id => area_id); + this.#options = { + ...this.#options, + ...options, + }; + } + + /** + * Create a Title card. + * + * @return {Object} A Title card. + */ + createCard() { + /** @type {Object[]} */ + const cards = [ + { + type: "custom:mushroom-title-card", + title: this.#options.title, + subtitle: this.#options.subtitle, + }, + ]; + + if (this.#options.showControls) { + cards.push({ + type: "horizontal-stack", + cards: [ + { + type: "custom:mushroom-template-card", + icon: this.#options.iconOff, + layout: "vertical", + icon_color: "red", + tap_action: { + action: "call-service", + service: this.#options.offService, + target: { + area_id: this.#areaIds, + }, + data: {}, + }, + }, + { + type: "custom:mushroom-template-card", + icon: this.#options.iconOn, + layout: "vertical", + icon_color: "amber", + tap_action: { + action: "call-service", + service: this.#options.onService, + target: { + area_id: this.#areaIds, + }, + data: {}, + }, + }, + ], + }); + } + + return { + type: "horizontal-stack", + cards: cards, + }; + } +} + +export {TitleCard}; diff --git a/src/cards/typedefs.js b/src/cards/typedefs.js new file mode 100644 index 0000000..a387a61 --- /dev/null +++ b/src/cards/typedefs.js @@ -0,0 +1,137 @@ +/** + * @namespace typedefs.cards + */ + +/** + * @typedef {Object} abstractOptions + * @property {string} [type] The type of the card. + * @property {string} [icon] Icon of the card. + * @property {Object} [double_tap_action] Home assistant action to perform on double_tap. + */ + +/** + * @typedef {Object} titleCardOptions Title Card options. + * @property {string} [title] Title to render. May contain templates. + * @property {string} [subtitle] Subtitle to render. May contain templates. + * @property {boolean} [showControls=true] False to hide controls. + * @property {string} [iconOn] Icon to show for switching entities from off state. + * @property {string} [iconOff] Icon to show for switching entities to off state. + * @property {string} [onService=none] Service to call for switching entities from off state. + * @property {string} [offService=none] Service to call for switching entities to off state. + * @memberOf typedefs.cards + */ + +/** + * @typedef {abstractOptions & Object} lightCardOptions Light Card options. + * @property {boolean} [show_brightness_control=true] Show a slider to control brightness + * @property {boolean} [show_color_control=true] Show a slider to control RGB color + * @property {boolean} [use_light_color=true] Colorize the icon and slider according light temperature or color + * @property {{double_tap_action: lightDoubleTapAction}} [action] Home assistant action to perform on double_tap + * @memberOf typedefs.cards + */ + +/** + * @typedef {Object} lightDoubleTapAction Home assistant action to perform on double_tap. + * @property {{entity_id: string}} target The target entity id. + * @property {"call-service"} action Calls a hass service. + * @property {"light.turn_on"} service The hass service to call + * @property {{rgb_color: [255, 255, 255]}} data The data payload for the service. + * @memberOf typedefs.cards + */ + +/** + * @typedef {abstractOptions & Object} coverCardOptions Cover Card options. + * @property {boolean} [show_buttons_control=true] Show buttons to open, close and stop cover. + * @property {boolean} [show_position_control=true] Show a slider to control position of the cover. + * @property {boolean} [show_tilt_position_control=true] Show a slider to control tilt position of the cover. + * @memberOf typedefs.cards + */ + +/** + * @typedef {abstractOptions & Object} fanCardOptions Fan Card options. + * @property {boolean} [show_percentage_control=true] Show a slider to control speed. + * @property {boolean} [show_oscillate_control=true] Show a button to control oscillation. + * @property {boolean} [icon_animation=true] Animate the icon when fan is on. + * @memberOf typedefs.cards + */ + +/** + * @typedef {abstractOptions & Object} switchCardOptions Switch Card options. + * @property {{tap_action: switchTapAction}} [action] Home assistant action to perform on tap. + * @memberOf typedefs.cards + */ + +/** + * @typedef {Object} switchTapAction Home assistant action to perform on tap. + * @property {"toggle"} action Toggles a hass entity. + * @memberOf typedefs.cards + */ + +/** + * @typedef {abstractOptions & Object} climateCardOptions Climate Card options. + * @property {["off", "cool", "heat", "fan_only"]} [hvac_modes] Show buttons to control target temperature. + * @property {boolean} [show_temperature_control=true] Show buttons to control target temperature. + * @memberOf typedefs.cards + */ + +/** + * @typedef {abstractOptions} cameraCardOptions Camera Card options. + * @memberOf typedefs.cards + */ + +/** + * @typedef {abstractOptions & Object} personCardOptions Person Card options. + * @property {string} [layout] Layout of the card. Vertical, horizontal, and default layouts are supported. + * @property {("name" | "state" | "last-changed" | "last-updated" | "none")} [primary_info=name] Info to show as + * primary info. + * @property {("name" | "state" | "last-changed" | "last-updated" | "none")} [secondary_info=sate] Info to show as + * secondary info. + * @property {("icon" | "entity-picture" | "none")} [icon_type]=icon Type of icon to display. + * @memberOf typedefs.cards + */ + +/** + * @typedef {abstractOptions & Object} areaCardOptions Area Card options. + * @property {string} [name] The name of the area + * @property {string} [icon] Icon to render. May contain templates. + * @property {string} [icon_color] Icon color to render. May contain templates. + * @property {string} [primary] Primary info to render. May contain templates. + * @property {areaTapAction} [tap_action] Home assistant action to perform on tap. + * @memberOf typedefs.cards + */ + +/** + * @typedef {Object} areaTapAction Home assistant action to perform on tap. + * @property {"navigate"} action Toggles a hass entity. + * @property {string} navigation_path The id of the area to navigate to. + * @memberOf typedefs.cards + */ + +/** + * @typedef {abstractOptions & Object} mediaPlayerCardOptions Media Player Card options. + * @property {boolean} [use_media_info=true] Use media info instead of name, state, and icon when a media is playing + * @property {string[]} [media_controls="on_off", "play_pause_stop"] List of controls to display + * (on_off, shuffle, previous, play_pause_stop, next, + * repeat) + * @property {boolean} [show_volume_level=true] Show volume level next to media state when media is playing + * @property {string[]} [volume_controls="volume_mute", "volume_set", "volume_buttons"] List of controls to display + * (volume_mute, volume_set, + * volume_buttons) + * @memberOf typedefs.cards + */ + +/** + * @typedef {abstractOptions & Object} sensorCardOptions Sensor Card options. + * @property {string} [icon_color=green] Custom color for icon when entity is state is active. + * @property {boolean} [animate=true] Add a reveal animation to the graph. + * @property {string} [line_color=green] Set a custom color for the graph line. + * Provide a list of colors for multiple graph entries. + * @memberOf typedefs.cards + */ + +/** + * @typedef {abstractOptions & Object} miscellaneousCardOptions Miscellaneous Card options. + * @property {string} [icon_color=blue-grey] Custom color for icon when entity is state is active. + * @memberOf typedefs.cards + */ + diff --git a/src/chips/ClimateChip.js b/src/chips/ClimateChip.js new file mode 100644 index 0000000..4ff3b68 --- /dev/null +++ b/src/chips/ClimateChip.js @@ -0,0 +1,35 @@ +import {Helper} from "../Helper"; + +class ClimateChip { + #areaIds; + #options = { + // No default options. + }; + + constructor(areaIds, options = {}) { + if (!Helper.isInitialized()) { + throw new Error("The Helper module must be initialized before using this one."); + } + + this.#areaIds = areaIds.filter(areaId => areaId); + this.#options = { + ...this.#options, + ...options, + }; + } + + getChip() { + return { + type: "template", + icon: "mdi:thermostat", + icon_color: "orange", + content: Helper.getCountTemplate("climate", "ne", "off"), + tap_action: { + action: "navigate", + navigation_path: "thermostats", + }, + }; + } +} + +export {ClimateChip}; diff --git a/src/chips/CoverChip.js b/src/chips/CoverChip.js new file mode 100644 index 0000000..0180239 --- /dev/null +++ b/src/chips/CoverChip.js @@ -0,0 +1,35 @@ +import {Helper} from "../Helper"; + +class CoverChip { + #areaIds; + #options = { + // No default options. + }; + + constructor(areaIds, options = {}) { + if (!Helper.isInitialized()) { + throw new Error("The Helper module must be initialized before using this one."); + } + + this.#areaIds = areaIds.filter(areaId => areaId); + this.#options = { + ...this.#options, + ...options, + }; + } + + getChip() { + return { + type: "template", + icon: "mdi:window-open", + icon_color: "cyan", + content: Helper.getCountTemplate("cover", "eq", "open"), + tap_action: { + action: "navigate", + navigation_path: "covers", + }, + }; + } +} + +export {CoverChip}; diff --git a/src/chips/FanChip.js b/src/chips/FanChip.js new file mode 100644 index 0000000..46880ed --- /dev/null +++ b/src/chips/FanChip.js @@ -0,0 +1,43 @@ +import {Helper} from "../Helper"; + +class FanChip { + #areaIds; + #options = { + // No default options. + }; + + constructor(areaIds, options = {}) { + if (!Helper.isInitialized()) { + throw new Error("The Helper module must be initialized before using this one."); + } + + this.#areaIds = areaIds.filter(areaId => areaId); + this.#options = { + ...this.#options, + ...options, + }; + } + + getChip() { + return { + type: "template", + icon: "mdi:fan", + icon_color: "green", + content: Helper.getCountTemplate("fan", "eq", "on"), + tap_action: { + action: "call-service", + service: "fan.turn_off", + target: { + area_id: this.#areaIds, + }, + data: {}, + }, + hold_action: { + action: "navigate", + navigation_path: "fans", + }, + }; + } +} + +export {FanChip}; diff --git a/src/chips/LightChip.js b/src/chips/LightChip.js new file mode 100644 index 0000000..557c502 --- /dev/null +++ b/src/chips/LightChip.js @@ -0,0 +1,43 @@ +import {Helper} from "../Helper"; + +class LightChip { + #areaIds; + #options = { + // No default options. + }; + + constructor(areaIds, options = {}) { + if (!Helper.isInitialized()) { + throw new Error("The Helper module must be initialized before using this one."); + } + + this.#areaIds = areaIds.filter(areaId => areaId); + this.#options = { + ...this.#options, + ...options, + }; + } + + getChip() { + return { + type: "template", + icon: "mdi:lightbulb-group", + icon_color: "amber", + content: Helper.getCountTemplate("light", "eq", "on"), + tap_action: { + action: "call-service", + service: "light.turn_off", + target: { + area_id: this.#areaIds, + }, + data: {}, + }, + hold_action: { + action: "navigate", + navigation_path: "lights", + }, + }; + } +} + +export {LightChip}; diff --git a/src/chips/SwitchChip.js b/src/chips/SwitchChip.js new file mode 100644 index 0000000..4e4eac0 --- /dev/null +++ b/src/chips/SwitchChip.js @@ -0,0 +1,43 @@ +import {Helper} from "../Helper"; + +class SwitchChip { + #areaIds; + #options = { + // No default options. + }; + + constructor(areaIds, options = {}) { + if (!Helper.isInitialized()) { + throw new Error("The Helper module must be initialized before using this one."); + } + + this.#areaIds = areaIds.filter(areaId => areaId); + this.#options = { + ...this.#options, + ...options, + }; + } + + getChip() { + return { + type: "template", + icon: "mdi:dip-switch", + icon_color: "blue", + content: Helper.getCountTemplate("switch", "eq", "on"), + tap_action: { + action: "call-service", + service: "switch.turn_off", + target: { + area_id: this.#areaIds, + }, + data: {}, + }, + hold_action: { + action: "navigate", + navigation_path: "switches", + }, + }; + } +} + +export {SwitchChip}; diff --git a/src/chips/WeatherChip.js b/src/chips/WeatherChip.js new file mode 100644 index 0000000..76a99f1 --- /dev/null +++ b/src/chips/WeatherChip.js @@ -0,0 +1,25 @@ +class WeatherChip { + #entityId; + #options = { + show_temperature: true, + show_conditions: true, + }; + + constructor(entityId, options = {}) { + this.#entityId = entityId; + this.#options = { + ...this.#options, + ...options, + }; + } + + getChip() { + return { + type: "weather", + entity: this.#entityId, + ...this.#options, + }; + } +} + +export {WeatherChip}; diff --git a/src/mushroom-strategy.js b/src/mushroom-strategy.js new file mode 100644 index 0000000..5acdf63 --- /dev/null +++ b/src/mushroom-strategy.js @@ -0,0 +1,246 @@ +import {Helper} from "./Helper"; +import {SensorCard} from "./cards/SensorCard"; +import {TitleCard} from "./cards/TitleCard"; + +/** + * Mushroom Dashboard Strategy.
+ *
+ * Mushroom dashboard strategy provides a strategy for Home-Assistant to create a dashboard automatically.
+ * The strategy makes use Mushroom, Mini Graph and WebRTC cards to represent your entities.
+ *
+ * Features:
+ * 🛠 Automatically create dashboard with 3 lines of yaml.
+ * 😍 Built-in Views for several standard domains.
+ * 🎨 Many options to customize to your needs.
+ *
+ * Check the [Repository]{@link https://github.com/AalianKhan/mushroom-strategy} for more information. + */ +class MushroomStrategy { + /** + * Generate a dashboard. + * + * Called when opening a dashboard. + * + * @param {dashBoardInfo} info Dashboard strategy information object. + * @return {Promise<{views: Object[]}>} + */ + static async generateDashboard(info) { + await Helper.initialize(info); + + // Create views. + const views = []; + + let viewModule; + + // Create a view for each exposed domain. + for (let viewId of Helper.getExposedViewIds()) { + try { + const viewType = Helper.sanitizeClassName(viewId + "View"); + viewModule = await import(`./views/${viewType}`); + const view = await new viewModule[viewType](Helper.strategyOptions.views[viewId]).getView(); + + views.push(view); + + } catch (e) { + console.error(Helper.debug ? e : `View '${viewId}' couldn't be loaded!`); + } + } + + // Create subviews for each area. + for (let area of Helper.areas) { + if (!area.hidden) { + views.push({ + title: area.name, + path: area.area_id ?? area.name, + subview: true, + strategy: { + type: "custom:mushroom-strategy", + options: { + area, + "entity_config": Helper.strategyOptions.entity_config, + }, + }, + }); + } + } + + // Add custom views. + if (Helper.strategyOptions.extra_views) { + views.push(...Helper.strategyOptions.extra_views); + } + + // Return the created views. + return { + views: views, + }; + } + + /** + * Generate a view. + * + * Called when opening a subview. + * + * @param {viewInfo} info The view's strategy information object. + * @return {Promise<{cards: Object[]}>} + */ + static async generateView(info) { + const exposedDomainIds = Helper.getExposedDomainIds(); + const area = info.view.strategy.options.area; + const viewCards = [...(area.extra_cards ?? [])]; + const strategyOptions = { + entityConfig: info.view.strategy.options.entity_config, + }; + + // Create cards for each domain. + for (const domain of exposedDomainIds) { + if (domain === "default") { + continue; + } + + const className = Helper.sanitizeClassName(domain + "Card"); + + let domainCards = []; + + try { + domainCards = await import(`./cards/${className}`).then(cardModule => { + let domainCards = []; + const entities = Helper.getDeviceEntities(area, domain); + + if (entities.length) { + // Create a Title card for the current domain. + const titleCard = new TitleCard( + [area], + Helper.strategyOptions.domains[domain] + ).createCard(); + + if (domain === "sensor") { + // Create a card for each entity-sensor of the current area. + const sensorStates = Helper.getStateEntities(area, "sensor"); + const sensorCards = []; + + for (const sensor of entities) { + let card = (strategyOptions.entityConfig?.find(config => config.entity_id === sensor.entity_id)); + + if (card) { + sensorCards.push(card); + continue; + } + + // Find the state of the current sensor. + const sensorState = sensorStates.find(state => state.entity_id === sensor.entity_id); + let cardOptions = {}; + + if (sensorState?.attributes.unit_of_measurement) { + cardOptions = { + type: "custom:mini-graph-card", + entities: [sensor.entity_id], + }; + } + + sensorCards.push(new SensorCard(sensor, cardOptions).getCard()); + } + + domainCards.push({ + type: "vertical-stack", + cards: sensorCards, + }); + + domainCards.unshift(titleCard); + return domainCards; + } + + // Create a card for each domain-entity of the current area. + for (const entity of entities) { + const card = (Helper.strategyOptions.entity_config ?? []).find( + config => config.entity === entity.entity_id, + ) ?? new cardModule[className](entity).getCard(); + + domainCards.push(card); + } + + if (domain === "binary_sensor") { + // Horizontally group every two binary sensor cards. + const horizontalCards = []; + + for (let i = 0; i < domainCards.length; i += 2) { + horizontalCards.push({ + type: "horizontal-stack", + cards: domainCards.slice(i, i + 2), + }); + } + + domainCards = horizontalCards; + } + + domainCards.unshift(titleCard); + } + + return domainCards; + }); + } catch (e) { + console.error(Helper.debug ? e : "An error occurred while creating the domain cards!"); + } + + if (domainCards.length) { + viewCards.push({ + type: "vertical-stack", + cards: domainCards, + }); + } + } + + // Create cards for any other domain. + // Collect device entities of the current area. + const areaDevices = Helper.devices.filter(device => device.area_id === area.area_id) + .map(device => device.id); + + // Collect the remaining entities of which all conditions below are met: + // 1. The entity is linked to a device which is linked to the current area, + // or the entity itself is linked to the current area. + // 2. The entity is not hidden and is not disabled. + const miscellaneousEntities = Helper.entities.filter(entity => { + return (areaDevices.includes(entity.device_id) || entity.area_id === area.area_id) + && entity.hidden_by == null + && entity.disabled_by == null + && !exposedDomainIds.includes(entity.entity_id.split(".", 1)[0]); + }); + + // Create a column of miscellaneous entity cards. + if (miscellaneousEntities.length) { + let miscellaneousCards = []; + + try { + miscellaneousCards = await import("./cards/MiscellaneousCard").then(cardModule => { + /** @type Object[] */ + const miscellaneousCards = [ + new TitleCard([area], Helper.strategyOptions.domains.default).createCard(), + ]; + for (const entity of miscellaneousEntities) { + const card = (Helper.strategyOptions.entity_config ?? []).find( + config => config.entity === entity.entity_id, + ) ?? new cardModule.MiscellaneousCard(entity).getCard(); + + miscellaneousCards.push(card); + } + + return miscellaneousCards; + }); + } catch (e) { + console.error(Helper.debug ? e : "An error occurred while creating the domain cards!"); + } + + viewCards.push({ + type: "vertical-stack", + cards: miscellaneousCards, + }); + } + + // Return cards. + return { + cards: viewCards, + }; + } +} + +// noinspection JSUnresolvedReference +customElements.define("ll-strategy-mushroom-strategy", MushroomStrategy); diff --git a/src/optionDefaults.js b/src/optionDefaults.js new file mode 100644 index 0000000..2933306 --- /dev/null +++ b/src/optionDefaults.js @@ -0,0 +1,110 @@ +export const optionDefaults = { + debug: false, + views: { + home: { + order: 1, + hidden: false, + }, + light: { + order: 2, + hidden: false, + }, + fan: { + order: 3, + hidden: false, + }, + cover: { + order: 4, + hidden: false, + }, + switch: { + order: 5, + hidden: false, + }, + climate: { + order: 6, + hidden: false, + }, + camera: { + order: 7, + hidden: false, + } + }, + areas: { + undisclosed: { + aliases: [], + area_id: null, + name: "Undisclosed", + picture: null, + hidden: false, + } + }, + domains: { + default: { + title: "Miscellaneous", + showControls: false, + hidden: false, + }, + light: { + title: "Lights", + showControls: true, + iconOn: "mdi:lightbulb", + iconOff: "mdi:lightbulb-off", + onService: "light.turn_on", + offService: "light.turn_off", + hidden: false, + }, + fan: { + title: "Fans", + showControls: true, + iconOn: "mdi:fan", + iconOff: "mdi:fan-off", + onService: "fan.turn_on", + offService: "fan.turn_off", + hidden: false, + }, + cover: { + title: "Covers", + showControls: true, + iconOn: "mdi:arrow-up", + iconOff: "mdi:arrow-down", + onService: "cover.open_cover", + offService: "cover.close_cover", + hidden: false, + }, + switch: { + title: "Switches", + showControls: true, + iconOn: "mdi:power-plug", + iconOff: "mdi:power-plug-off", + onService: "switch.turn_on", + offService: "switch.turn_off", + hidden: false, + }, + camera: { + title: "Cameras", + showControls: false, + hidden: false, + }, + climate: { + title: "Climates", + showControls: false, + hidden: false, + }, + media_player: { + title: "Media Players", + showControls: false, + hidden: false, + }, + sensor: { + title: "Sensors", + showControls: false, + hidden: false, + }, + binary_sensor: { + title: "Binary Sensors", + showControls: false, + hidden: false, + }, + } +} diff --git a/src/typedefs.js b/src/typedefs.js new file mode 100644 index 0000000..de7e7f5 --- /dev/null +++ b/src/typedefs.js @@ -0,0 +1,247 @@ +/** + * @namespace typedefs.generic + */ + +/** + * @typedef {Object} hassEntity Home assistant entity. + * @property {string} name The name of this entity. + * @property {string} original_name The original name of this entity. + * @property {string} entity_id The id of this entity. + * @property {string} device_id The id of the device to which this entity is linked. + * @property {string} area_id The id of the area to which this entity is linked. + * @property {string[]|null} disabled_by Indicates by what this entity is disabled. + * @property {string[]|null} hidden_by Indicates by what this entity is hidden. + * @memberOf typedefs.generic + */ + +/** + * @typedef {Object} deviceEntity Device Entity. + * @property {string} area_id The Area which the device is placed in. + * @property {string} id Unique ID of a device (generated by Home Assistant). + * @property {string[]|null} disabled_by Indicates by what this entity is disabled. + * @property {string[]|null} hidden_by Indicates by what this entity is hidden. + * @memberOf typedefs.generic + */ + +/** + * @typedef {Object} areaEntity Area Entity. + * @property {string[]} [aliases] Array of aliases of this entity. + * @property {string|null} area_id The id of this entity. + * @property {string} name Name of this entity. + * @property {string|null} picture URL to a picture that should be used instead of showing the domain icon. + * @property {number} [order] Ordering position of the area in the list of available areas. + * @property {boolean} [hidden] True if the entity should be hidden from the dashboard. + * This property is added by the custom strategy. + * @property {Object[]} [extra_cards] An array of card configurations. + * The configured cards are added to the dashboard. + * This property is added by the custom strategy. + * @memberOf typedefs.generic + */ + +/** + * @typedef {Object} viewEntity View Entity. + * This entity is added by the custom strategy. + * @property {string} title Title of this entity. + * @property {string} icon Icon to use for the entity in the frontend. + * Example: `mdi:home`. + * @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. + * @memberOf typedefs.generic + */ + +/** + * @typedef {Object & titleCardOptions} domainEntity Domain Entity. + * This entity is added by the custom 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. + * @memberOf typedefs.generic + */ + +/** + * @typedef {Object} titleCardOptions Title Card options. + * @property {string} [title] Title to render. May contain templates. + * @property {string} [subtitle] Subtitle to render. May contain templates. + * @property {boolean} [showControls=true] False to hide controls. + * @property {string} [iconOn] Icon to show for switching entities from off state. + * @property {string} [iconOff] Icon to show for switching entities to off state. + * @property {string} [onService=none] Service to call for switching entities from off state. + * @property {string} [offService=none] Service to call for switching entities to off state. + * @memberOf typedefs.generic + */ + +/** + * @typedef {Object} dashBoardInfo Strategy information object. + * @property {dashboardConfig} config User supplied dashboard configuration, if any. + * @property {hassObject} hass The Home Assistant object. + * @property {boolean} narrow If the current user interface is rendered in narrow mode or not. + * @memberOf typedefs.generic + * @see https://developers.home-assistant.io/docs/frontend/custom-ui/custom-strategy/#dashboard-strategies + */ + +/** + * @typedef {Object} viewInfo Strategy information object. + * @property {Object} view Configuration of the current view. + * @property {viewConfig} config Dashboard configuration. + * @property {hassObject} hass The Home Assistant object. + * @property {boolean} narrow If the current user interface is rendered in narrow mode or not. + * @memberOf typedefs.generic + * @see https://developers.home-assistant.io/docs/frontend/custom-ui/custom-strategy/#view-strategies + */ + +/** + * @typedef {Object} dashboardConfig User supplied dashboard configuration. + * @property {strategyObject} strategy User supplied dashboard configuration. + * @memberOf typedefs.generic + */ + +/** + * @typedef {Object} viewConfig Dashboard configuration. + * @property {Object[]} strategy Array of views generated by the strategy. + * @memberOf typedefs.generic + */ + +/** + * @typedef {Object} strategyObject User supplied dashboard configuration. + * @property {strategyOptions} options Custom strategy configuration. + * @property {string} type Strategy type. + * @memberOf typedefs.generic + */ + +/** + * @typedef {Object} customStrategyOptions Custom strategy configuration. + * @property {boolean} [debug] Set to true for more verbose debugging info. + * @property {Object.} [areas] List of areas. + * @property {Object[]} [entity_config] Card definition for entities. + * @property {Object.} [views] List of views. + * @property {Object.} [domains] List of domains. + * @property {chip[]} [chips] List of chips to show in the Home view. + * @property {Object[]} [quick_access_cards] List of cards to show between welcome card and rooms cards. + * @property {Object[]} [extra_cards] List of cards to show below room cards. + * @property {Object[]} [extra_views] List of views to add to the dashboard. + * @memberOf typedefs.generic + */ + +/** + * @typedef {Object} chip 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. + * @property {boolean} climate_count Chip to display the number of climates which are not off. + * @property {string} weather_entity Entity ID for the weather chip to use, accepts `weather.` only. + * @property {Object[]} extra_chips List of extra chips. + * @memberOf typedefs.generic + */ + +/** + * @typedef {Object} entityConfig Custom card-configuration for an entity on a view card. + * @property {string} entity The id of the entity to create a card for. + * @property {string} type Type of card for the entity + * @memberOf typedefs.generic + */ + +/** + * The frontend passes a single hass object around. + * This object contains the latest state and allows you to send commands back to the server. + * + * @typedef {Object} hassObject Home Assistant object. + * @property {Object} states An object containing the states of all entities in Home Assistant. + * The key is the entity_id, the value is the state object. + * @property {hassUser} user The logged-in user. + * @property {function} callWS Call a WebSocket command on the backend. + * @memberOf typedefs.generic + * @see https://developers.home-assistant.io/docs/frontend/data/ + */ + +/** + * The logged-in user. + * + * @typedef {Object} hassUser The logged-in user. + * @property {string} name Name of the user. + * @property {boolean} is_owner True if the user is the owner. + * @property {boolean} is_owner True if the user is an administrator. + * @property {Object[]} credentials Authentication credentials. + * @memberOf typedefs.generic + * @see https://developers.home-assistant.io/docs/frontend/data/#hassuser + */ + +/** + * States are a current representation of the entity. + * + * All states will always have an entity id, a state and a timestamp when last updated and last changed. + * + * @typedef {Object} stateObject State object. + * @property {string} state String representation of the entity's current state. + * Example `off`. + * @property {string} entity_id Entity ID. + * Format: .. + * Example: `light.kitchen`. + * @property {string} domain Domain of the entity. + * Example: `light`. + * @property {string} object_id Object ID of entity. + * Example: `kitchen`. + * @property {string} name Name of the entity. + * Based on `friendly_name` attribute with fall back to object ID. + * Example: `Kitchen Ceiling`. + * @property {string} last_updated Time the state was written to the state machine in UTC time. + * Note that writing the exact same state including attributes will not result in this + * field being updated. + * Example: `2017-10-28 08:13:36.715874+00:00`. + * @property {string} last_changed Time the state changed in the state machine in UTC time. + * This is not updated when there are only updated attributes. + * Example: `2017-10-28 08:13:36.715874+00:00`. + * @property {stateAttributes} attributes A dictionary with extra attributes related to the current state. + * @property {stateContext} context A dictionary with extra attributes related to the context of the state. + * @memberOf typedefs.generic + * @see https://www.home-assistant.io/docs/configuration/state_object/ + */ + +/** + * The attributes of an entity are optional. + * + * There are a few attributes that are used by Home Assistant for representing the entity in a specific way. + * Each integration will also have its own attributes to represent extra state data about the entity. + * For example, the light integration has attributes for the current brightness and color of the light. + * + * When an attribute is not available, Home Assistant will not write it to the state. + * + * @typedef {Object} stateAttributes State attributes. + * @property {string} friendly_name Name of the entity. + * Example: `Kitchen Ceiling`. + * @property {string} icon Icon to use for the entity in the frontend. + * Example: `mdi:home`. + * @property {string} entity_picture URL to a picture that should be used instead of showing the domain icon. + * @property {string} assumed_state Boolean if the current state is an assumption. + * @property {string} unit_of_measurement The unit of measurement the state is expressed in. + * Used for grouping graphs or understanding the entity. + * Example: `°C`. + * @memberOf typedefs.generic + * @see https://www.home-assistant.io/docs/configuration/state_object/#attributes + */ + +/** + * Context is used to tie events and states together in Home Assistant. Whenever an automation or user interaction + * causes states to change, a new context is assigned. This context will be attached to all events and states that + * happen as a result of the change. + * + * @typedef {Object} stateContext State context. + * @property {string} context_id Unique identifier for the context. + * @property {string} user_id Unique identifier of the user that started the change. + * Will be None if action was not started by a user (i.e. started by an automation) + * @property {string} parent_id Unique identifier of the parent context that started the change, if available. + * For example, if an automation is triggered, the context of the trigger will be set as + * parent. + * @see https://www.home-assistant.io/docs/configuration/state_object/#context + * @memberOf typedefs.generic + */ + +/** + * @typedef {Object} areaFilterContext fer Card options. + * @property {areaEntity} area Area Entity. + * @property {string} domain Domain of the entity. + * Example: `light`. + * @property {string[]} areaDeviceIds The id of devices which are linked to the area entity. + * @memberOf typedefs.cards + */ + +export {}; diff --git a/src/views/AbstractView.js b/src/views/AbstractView.js new file mode 100644 index 0000000..e87c7aa --- /dev/null +++ b/src/views/AbstractView.js @@ -0,0 +1,121 @@ +import {Helper} from "../Helper"; +import {TitleCard} from "../cards/TitleCard"; + +/** + * Abstract View Class. + * + * To create a new view, extend the new class with this one. + * + * @class + * @abstract + */ +class AbstractView { + /** + * Options for creating a view. + * + * @type {abstractOptions} + */ + options = { + title: null, + path: null, + icon: "mdi:view-dashboard", + subview: false, + }; + + /** + * A card to switch all entities in the view. + * + * @type {Object} + */ + viewTitleCard; + + /** + * Class constructor. + * + * @throws {Error} If trying to instantiate this class. + * @throws {Error} If the Helper module isn't initialized. + */ + constructor() { + if (this.constructor === AbstractView) { + throw new Error("Abstract classes can't be instantiated."); + } + + if (!Helper.isInitialized()) { + throw new Error("The Helper module must be initialized before using this one."); + } + } + + /** + * Merge the default options of this class and the custom options into the options of the parent class. + * + * @param {Object} [defaultOptions={}] Default options for the card. + * @param {Object} [customOptions={}] Custom Options for the card. + */ + mergeOptions(defaultOptions, customOptions) { + this.options = { + ...defaultOptions, + ...customOptions, + }; + } + + /** + * Create the cards to include in the view. + * + * @return {Object[] | Promise} An array of card objects. + */ + createViewCards() { + /** @type Object[] */ + const viewCards = [this.viewTitleCard]; + + // Create cards for each area. + for (const area of Helper.areas) { + const areaCards = []; + const entities = Helper.getDeviceEntities(area, this["domain"]); + const className = Helper.sanitizeClassName(this["domain"] + "Card"); + + import((`../cards/${className}`)).then(cardModule => { + if (entities.length) { + // Create a Title card for the current area. + areaCards.push( + new TitleCard([area], { + title: area.name, + ...this.options["titleCard"], + }).createCard(), + ); + + // Create a card for each domain-entity of the current area. + for (const entity of entities) { + const card = (Helper.strategyOptions.entity_config ?? []).find( + config => config.entity === entity.entity_id, + ) ?? new cardModule[className](entity).getCard(); + + areaCards.push(card); + } + } + }); + + viewCards.push({ + type: "vertical-stack", + cards: areaCards, + }); + } + + return viewCards; + } + + /** + * Get a view object. + * + * The view includes the cards which are created by method createViewCards(). + * + * @returns {viewOptions & {cards: Object[]}} The view object. + */ + async getView() { + return { + ...this.options, + cards: await this.createViewCards(), + }; + } +} + +export {AbstractView}; diff --git a/src/views/CameraView.js b/src/views/CameraView.js new file mode 100644 index 0000000..bbb8cf8 --- /dev/null +++ b/src/views/CameraView.js @@ -0,0 +1,70 @@ +import {Helper} from "../Helper"; +import {TitleCard} from "../cards/TitleCard"; +import {AbstractView} from "./AbstractView"; + +/** + * Camera View Class. + * + * Used to create a view for entities of the camera domain. + * + * @class CameraView + * @extends AbstractView + */ +class CameraView extends AbstractView { + /** + * Domain of the view's entities. + * @type {string} + */ + #domain = "camera"; + + /** + * Default options for the view. + * + * @type {viewOptions} + * @private + */ + #defaultOptions = { + title: "Cameras", + path: "cameras", + icon: "mdi:cctv", + subview: false, + titleCard: { + showControls: false, + }, + }; + + /** + * Options for the view's title card. + * + * @type {viewTitleCardOptions} + */ + #viewTitleCardOption = { + title: "All Cameras", + ...this.options["titleCard"], + }; + + /** + * Class constructor. + * + * @param {viewOptions} [options={}] Options for the view. + */ + constructor(options = {}) { + super(); + this.mergeOptions( + this.#defaultOptions, + options, + ); + + // Create a title card to switch all entities of the domain. + this.viewTitleCard = new TitleCard(Helper.areas, { + ...this.#viewTitleCardOption, + ...this.options["titleCard"], + }).createCard(); + } + + get domain() { + return this.#domain; + } +} + +export {CameraView}; diff --git a/src/views/ClimateView.js b/src/views/ClimateView.js new file mode 100644 index 0000000..16c164b --- /dev/null +++ b/src/views/ClimateView.js @@ -0,0 +1,71 @@ +import {Helper} from "../Helper"; +import {TitleCard} from "../cards/TitleCard"; +import {AbstractView} from "./AbstractView"; + +/** + * Climate View Class. + * + * Used to create a view for entities of the climate domain. + * + * @class ClimateView + * @extends AbstractView + */ +class ClimateView extends AbstractView { + /** + * Domain of the view's entities. + * @type {string} + */ + #domain = "climate"; + + /** + * Default options for the view. + * + * @type {viewOptions} + * @private + */ + #defaultOptions = { + title: "Climates", + path: "climates", + icon: "mdi:thermostat", + subview: false, + titleCard: { + showControls: false, + }, + }; + + /** + * Options for the view's title card. + * + * @type {viewTitleCardOptions} + */ + #viewTitleCardOption = { + title: "All Climates", + subtitle: Helper.getCountTemplate(this.domain, "ne", "off") + " climates on", + ...this.options["titleCard"], + }; + + /** + * Class constructor. + * + * @param {viewOptions} [options={}] Options for the view. + */ + constructor(options = {}) { + super(); + this.mergeOptions( + this.#defaultOptions, + options, + ); + + // Create a title card to switch all entities of the domain. + this.viewTitleCard = new TitleCard(Helper.areas, { + ...this.#viewTitleCardOption, + ...this.options["titleCard"], + }).createCard(); + } + + get domain() { + return this.#domain; + } +} + +export {ClimateView}; diff --git a/src/views/CoverView.js b/src/views/CoverView.js new file mode 100644 index 0000000..8abfade --- /dev/null +++ b/src/views/CoverView.js @@ -0,0 +1,73 @@ +import {Helper} from "../Helper"; +import {TitleCard} from "../cards/TitleCard"; +import {AbstractView} from "./AbstractView"; + +/** + * Cover View Class. + * + * Used to create a view for entities of the cover domain. + * + * @class CoverView + * @extends AbstractView + */ +class CoverView extends AbstractView { + /** + * Domain of the view's entities. + * @type {string} + */ + #domain = "cover"; + + /** + * Default options for the view. + * + * @type {viewOptions} + * @private + */ + #defaultOptions = { + title: "Covers", + path: "covers", + icon: "mdi:window-open", + subview: false, + titleCard: { + iconOn: "mdi:arrow-up", + iconOff: "mdi:arrow-down", + onService: "cover.open_cover", + offService: "cover.close_cover", + }, + }; + + /** + * Options for the view's title card. + * + * @type {viewTitleCardOptions} + */ + #viewTitleCardOption = { + title: "All Covers", + subtitle: Helper.getCountTemplate(this.domain, "eq", "open") + " covers open", + }; + + /** + * Class constructor. + * + * @param {viewOptions} [options={}] Options for the view. + */ + constructor(options = {}) { + super(); + this.mergeOptions( + this.#defaultOptions, + options, + ); + + // Create a title card to switch all entities of the domain. + this.viewTitleCard = new TitleCard(Helper.areas, { + ...this.#viewTitleCardOption, + ...this.options["titleCard"], + }).createCard(); + } + + get domain() { + return this.#domain; + } +} + +export {CoverView}; diff --git a/src/views/FanView.js b/src/views/FanView.js new file mode 100644 index 0000000..a0f42a6 --- /dev/null +++ b/src/views/FanView.js @@ -0,0 +1,73 @@ +import {Helper} from "../Helper"; +import {TitleCard} from "../cards/TitleCard"; +import {AbstractView} from "./AbstractView"; + +/** + * Fan View Class. + * + * Used to create a view for entities of the fan domain. + * + * @class FanView + * @extends AbstractView + */ +class FanView extends AbstractView { + /** + * Domain of the view's entities. + * @type {string} + */ + #domain = "fan"; + + /** + * Default options for the view. + * + * @type {viewOptions} + * @private + */ + #defaultOptions = { + title: "Fans", + path: "fans", + icon: "mdi:fan", + subview: false, + titleCard: { + iconOn: "mdi:fan", + iconOff: "mdi:fan-off", + onService: "fan.turn_on", + offService: "fan.turn_off", + }, + }; + + /** + * Options for the view's title card. + * + * @type {viewTitleCardOptions} + */ + #viewTitleCardOption = { + title: "All Fans", + subtitle: Helper.getCountTemplate(this.domain, "eq", "on") + " fans on", + }; + + /** + * Class constructor. + * + * @param {viewOptions} [options={}] Options for the view. + */ + constructor(options = {}) { + super(); + this.mergeOptions( + this.#defaultOptions, + options, + ); + + // Create a title card to switch all entities of the domain. + this.viewTitleCard = new TitleCard(Helper.areas, { + ...this.#viewTitleCardOption, + ...this.options["titleCard"], + }).createCard(); + } + + get domain() { + return this.#domain; + } +} + +export {FanView}; diff --git a/src/views/HomeView.js b/src/views/HomeView.js new file mode 100644 index 0000000..d03a1e1 --- /dev/null +++ b/src/views/HomeView.js @@ -0,0 +1,205 @@ +import {Helper} from "../Helper"; +import {AbstractView} from "./AbstractView"; + +/** + * Home View Class. + * + * Used to create a Home view. + * + * @class HomeView + * @extends AbstractView + */ +class HomeView extends AbstractView { + /** + * Default options for the view. + * + * @type {viewOptions} + * @private + */ + #defaultOptions = { + title: "Home", + path: "home", + subview: false, + }; + + /** + * Class constructor. + * + * @param {viewOptions} [options={}] Options for the view. + */ + constructor(options = {}) { + super(); + this.mergeOptions( + this.#defaultOptions, + options, + ); + } + + /** + * Create the cards to include in the view. + * + * @return {Promise} A promise of a card object array. + * @override + */ + async createViewCards() { + return await Promise.all([ + this.#createChips(), + this.#createPersonCards(), + this.#createAreaCards(), + ]).then(([chips, personCards, areaCards]) => { + const options = Helper.strategyOptions; + const homeViewCards = [ + { + type: "custom:mushroom-chips-card", + alignment: "center", + chips: chips, + }, + { + type: "horizontal-stack", + cards: personCards, + }, + { + type: "custom:mushroom-template-card", + primary: "{% set time = now().hour %} {% if (time >= 18) %} Good Evening, {{user}}! {% elif (time >= 12) %} Good Afternoon, {{user}}! {% elif (time >= 5) %} Good Morning, {{user}}! {% else %} Hello, {{user}}! {% endif %}", + icon: "mdi:hand-wave", + icon_color: "orange", + tap_action: { + action: "none", + }, + double_tap_action: { + action: "none", + }, + hold_action: { + action: "none", + }, + }, + ]; + + // Add quick access cards. + if (options.quick_access_cards) { + homeViewCards.push(...options.quick_access_cards); + } + + // Add area cards. + homeViewCards.push({ + type: "custom:mushroom-title-card", + title: "Areas", + }, + { + type: "vertical-stack", + cards: areaCards, + }); + + // Add custom cards. + if (options.extra_cards) { + homeViewCards.push(...options.extra_cards); + } + + return homeViewCards; + }); + } + + /** + * Create the chips to include in the view. + * + * @return {Object[]} A chip object array. + */ + async #createChips() { + const chips = []; + const chipOptions = Helper.strategyOptions.chips; + + // TODO: Get domains from config. + const exposed_chips = ["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( + entity => entity.entity_id.startsWith("weather.") && entity.disabled_by == null && entity.hidden_by == null, + ).entity_id; + + if (weatherEntityId) { + try { + chipModule = await import("../chips/WeatherChip"); + const weatherChip = new chipModule.WeatherChip(weatherEntityId); + chips.push(weatherChip.getChip()); + } catch (e) { + console.error(Helper.debug ? e : "An error occurred while creating the weather chip!"); + } + } + + // Numeric chips. + for (let chipType of exposed_chips) { + if (chipOptions?.[`${chipType}_count`] ?? true) { + const className = Helper.sanitizeClassName(chipType + "Chip"); + try { + chipModule = await import((`../chips/${className}`)); + const chip = new chipModule[className](areaIds); + chips.push(chip.getChip()); + } catch (e) { + console.error(Helper.debug ? e : `An error occurred while creating the ${chipType} chip!`); + } + } + } + + // Extra chips. + if (chipOptions?.extra_chips) { + chips.push(...chipOptions.extra_chips); + } + + return chips; + } + + /** + * Create the person cards to include in the view. + * + * @return {Object[]} A card object array. + */ + #createPersonCards() { + const cards = []; + + import("../cards/PersonCard").then(personModule => { + for (const person of Helper.entities.filter(entity => entity.entity_id.startsWith("person."))) { + cards.push(new personModule.PersonCard(person).getCard()); + } + }); + + return cards; + } + + /** + * Create the area cards to include in the view. + * + * Area cards are grouped into two areas per row. + * + * @return {Object[]} A card object array. + */ + #createAreaCards() { + const groupedCards = []; + + import("../cards/AreaCard").then(areaModule => { + const areaCards = []; + + for (const area of Helper.areas) { + if (!Helper.strategyOptions.areas[area.area_id]?.hidden) { + areaCards.push( + new areaModule.AreaCard(area, Helper.strategyOptions.areas[area.area_id ?? "undisclosed"]).getCard()); + } + } + + // Horizontally group every two area cards. + for (let i = 0; i < areaCards.length; i += 2) { + groupedCards.push({ + type: "horizontal-stack", + cards: areaCards.slice(i, i + 2), + }); + } + }); + + return groupedCards; + } +} + +export {HomeView}; diff --git a/src/views/LightView.js b/src/views/LightView.js new file mode 100644 index 0000000..1a985a1 --- /dev/null +++ b/src/views/LightView.js @@ -0,0 +1,73 @@ +import {Helper} from "../Helper"; +import {TitleCard} from "../cards/TitleCard"; +import {AbstractView} from "./AbstractView"; + +/** + * Light View Class. + * + * Used to create a view for entities of the light domain. + * + * @class LightView + * @extends AbstractView + */ +class LightView extends AbstractView { + /** + * Domain of the view's entities. + * @type {string} + */ + #domain = "light"; + + /** + * Default options for the view. + * + * @type {viewOptions} + * @private + */ + #defaultOptions = { + title: "Lights", + path: "lights", + icon: "mdi:lightbulb-group", + subview: false, + titleCard: { + iconOn: "mdi:lightbulb", + iconOff: "mdi:lightbulb-off", + onService: "light.turn_on", + offService: "light.turn_off", + }, + }; + + /** + * Options for the view's title card. + * + * @type {viewTitleCardOptions} + */ + #viewTitleCardOption = { + title: "All Lights", + subtitle: Helper.getCountTemplate(this.domain, "eq", "on") + " lights on", + }; + + /** + * Class constructor. + * + * @param {viewOptions} [options={}] Options for the view. + */ + constructor(options = {}) { + super(); + this.mergeOptions( + this.#defaultOptions, + options, + ); + + // Create a title card to switch all entities of the domain. + this.viewTitleCard = new TitleCard(Helper.areas, { + ...this.#viewTitleCardOption, + ...this.options["titleCard"], + }).createCard(); + } + + get domain() { + return this.#domain; + } +} + +export {LightView}; diff --git a/src/views/SwitchView.js b/src/views/SwitchView.js new file mode 100644 index 0000000..df4dcbb --- /dev/null +++ b/src/views/SwitchView.js @@ -0,0 +1,73 @@ +import {Helper} from "../Helper"; +import {TitleCard} from "../cards/TitleCard"; +import {AbstractView} from "./AbstractView"; + +/** + * Switch View Class. + * + * Used to create a view for entities of the switch domain. + * + * @class SwitchView + * @extends AbstractView + */ +class SwitchView extends AbstractView { + /** + * Domain of the view's entities. + * @type {string} + */ + #domain = "switch"; + + /** + * Default options for the view. + * + * @type {viewOptions} + * @private + */ + #defaultOptions = { + title: "Switches", + path: "switches", + icon: "mdi:dip-switch", + subview: false, + titleCard: { + iconOn: "mdi:power-plug", + iconOff: "mdi:power-plug-off", + onService: "switch.turn_on", + offService: "switch.turn_off", + }, + }; + + /** + * Options for the view's title card. + * + * @type {viewTitleCardOptions} + */ + #viewTitleCardOption = { + title: "All Switches", + subtitle: Helper.getCountTemplate(this.domain, "eq", "on") + " switches on", + }; + + /** + * Class constructor. + * + * @param {viewOptions} [options={}] Options for the view. + */ + constructor(options = {}) { + super(); + this.mergeOptions( + this.#defaultOptions, + options, + ); + + // Create a title card to switch all entities of the domain. + this.viewTitleCard = new TitleCard(Helper.areas, { + ...this.#viewTitleCardOption, + ...this.options["titleCard"], + }).createCard(); + } + + get domain() { + return this.#domain; + } +} + +export {SwitchView}; diff --git a/src/views/typedefs.js b/src/views/typedefs.js new file mode 100644 index 0000000..8466730 --- /dev/null +++ b/src/views/typedefs.js @@ -0,0 +1,38 @@ +/** + * @namespace typedefs.views + */ + +/** + * @typedef {Object} abstractOptions Options to create a view. + * @property {string} [title] The title or name. + * @property {string} [path] Paths are used in the URL. + * @property {string} [icon] The icon of the view. + * @property {boolean} subview Mark the view as “Subview”. + * @memberOf typedefs.views + * @see https://www.home-assistant.io/dashboards/views/ + */ + +/** + * @typedef {abstractOptions & Object} viewOptions Options for the extended View class. + * @property {titleCardOptions} [titleCard] Options for the title card of the view. + * @memberOf typedefs.views + */ + +/** + * @typedef {Object} titleCardOptions Options for the title card of the view. + * @property {string} iconOn Icon to show for switching entities from off state. + * @property {string} iconOff Icon to show for switching entities to off state. + * @property {string} onService Service to call for switching entities from off state. + * @property {string} offService Service to call for switching entities to off state. + * @memberOf typedefs.views + */ + +/** + * @typedef {Object} viewTitleCardOptions Options for the view's title card. + * @property {string} [title] Title to render. May contain templates. + * @property {string} [subtitle] Subtitle to render. May contain templates. + * @property {boolean} [showControls=true] False to hide controls. + * @memberOf typedefs.views + */ + +export {}; diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..ecc70a9 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,17 @@ +const path = require("path"); +const webpack = require("webpack"); + +module.exports = { + mode: "production", + entry: "./src/mushroom-strategy.js", + output: { + filename: "mushroom-strategy.js", + path: path.resolve(__dirname, "dist"), + clean: true, + }, + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + ], +}; diff --git a/webpack.dev.config.js b/webpack.dev.config.js new file mode 100644 index 0000000..0696846 --- /dev/null +++ b/webpack.dev.config.js @@ -0,0 +1,20 @@ +const path = require("path"); +const webpack = require("webpack"); + +module.exports = { + mode: "development", + devtool: "source-map", + entry: "./src/mushroom-strategy.js", + output: { + filename: "mushroom-strategy.js", + path: path.resolve(__dirname, "dist"), + }, + optimization: { + minimize: false, + }, + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + ], +};