Compare commits

...

213 Commits

Author SHA1 Message Date
Bram Kragten 081a13b3d7 Add back path to protocol config pages 2026-02-03 14:28:12 +01:00
Paul Bottein 19dc2a5865 Add missing danger variant in dropdown item (#29359) 2026-02-03 11:37:42 +00:00
Wendelin ae63530123 Migrate ha-control-select-menu to use ha-dropdown (#29350) 2026-02-03 10:30:51 +00:00
uptimeZERO_ 97e1f47af9 Add transition for more-info-dialog when toggling expand state (#29341) 2026-02-03 09:21:29 +00:00
Paul Bottein 346d916944 Add an disable view transition as it crashes chrome browser when using chrome dev tools (#29339) 2026-02-03 09:10:16 +01:00
Kristel ab326b3277 Refactor _applyFilters for Scenes and Scripts pages (#29116)
* proper functionnames

* refactor _applyFilters on scenes and script page

* Apply suggestions from code review

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-03 07:50:02 +00:00
Tom 55e251c04f Expose target_humidity_step value to climate (#28834)
Add target_humidity_step to climate
2026-02-03 09:34:53 +02:00
Darryn Capes-Davis b902f3e6f5 Fix CSS minification issue for ha-card (#29354) 2026-02-03 09:19:06 +02:00
ildar170975 9daac6c49f Remove "type" where "attribute: false" (#29356)
remove "type" when "attribute: false"
2026-02-03 09:17:33 +02:00
Benedikt Johannes 65ea0c9121 Fix "unamed" -> "unnamed" (potential crash / non registered translation information bug) (#29351)
* Fix typo

* Update home-area-view-strategy.ts

* Update home-other-devices-view-strategy.ts
2026-02-03 05:55:11 +00:00
Denis Shulyaka 505cc698f6 Fallback data flow section label translation (#29352) 2026-02-03 05:54:18 +00:00
karwosts b0f9b31dae Fix missing imports in devtools-statistics (#29355) 2026-02-03 06:45:39 +01:00
Norbert Rittel 2b2966a214 Make explanation of "search command(s)" shortcut consistent (#29345)
Change "search command" to "search commands"
2026-02-02 17:49:45 +01:00
renovate[bot] 558b251e32 Update dependency @codemirror/view to v6.39.12 (#29349)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 17:41:06 +01:00
renovate[bot] df28eaa99e Update dependency tar to v7.5.7 [SECURITY] (#29348)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 17:40:45 +01:00
Petar Petrov 40342c9cfd Remove redundant dialog backdrop color (#29337) 2026-02-02 17:12:27 +01:00
Paul Bottein c6573b9c1b Fix "Reload resources" menu for YAML resource mode (#29346) 2026-02-02 16:07:10 +00:00
karwosts 54c46ad362 Entity diagnostic - handle entity not in the registry (#29344) 2026-02-02 17:00:26 +01:00
Wendelin 3376036392 Fix dropdown width in datatables (#29340) 2026-02-02 13:49:54 +00:00
Paul Bottein ff5ecc047a Fix type error for missing hass.themes race condition in themes mixin (#29338) 2026-02-02 13:44:24 +00:00
Jeremy Cook a8393cddd4 Improved HTML presentation table support to ha-markdown, compatible w… (#29108)
Improved HTML presentation table support to ha-markdown, compatible with companion app on android and iOS

- Support valign attribute for vertical alignment (top, middle, bottom, baseline)
- Support border attribute for cell borders (0, 1, 2, 3px)
- Set default styling for presentation tables (no borders, no padding)
- Use CSS variables for customization
- Include future-proof @supports rule for attr() function
- Maintain backward compatibility with existing markdown tables
2026-02-02 15:17:12 +02:00
Tom Carpenter edfc33039b Merge Long Term Statistics for Power Sensors in Energy Dashboard (#29319)
* Merge Long Term Statistics for Power Sensors in Energy Dashboard

When using 5minute data for the power sources chart, data would be missing if the selected range was beyond the short term statistics limit. This change takes data from long term statistics and merges it in to the power sources data if the short term statistics doesn't extend far enough back for the selection.

* Skip for zero-length power statistics

Prevent out of bounds array access if power statistics has no entries.
2026-02-02 15:11:52 +02:00
ildar170975 14f8d982a9 hui-gauge-card-editor: use imported code for actions (#29326)
use imported code for actions
2026-02-02 15:02:22 +02:00
Marcin Bauer 847a040fa7 Keep focus on search field when clicking filter chips (#29249)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 13:45:54 +01:00
Aidan Timson 8611359481 Ensure template renderer overflows on overflow (#29335) 2026-02-02 12:07:49 +00:00
Kristel f473ebf18c bugfix: add eventlistener for exposed-entities-changed to Entities page (#29299) 2026-02-02 11:32:32 +00:00
Wendelin 9010898742 Fix missing ha-md-menu in config/labels (#29334) 2026-02-02 11:15:33 +00:00
Petar Petrov 1e30394bf3 Append current entity state to history and statistics charts (#29273) 2026-02-02 09:34:50 +00:00
Paulus Schoutsen bca2cb0c1e Conditionally show HTML5 push notifications (#29227) 2026-02-02 09:24:42 +00:00
Aidan Timson 29317eb842 Show hint only if keyboard shortcuts is enabled (#29332)
Enabled by default, must be explicity disabled
2026-02-02 10:59:14 +02:00
Wendelin a8327ef59a Revert "Fix automation sidebar ui supported check" (#29331) 2026-02-02 08:57:19 +00:00
Petar Petrov 01c8832024 Add area dashboard link to navigation picker (#29264) 2026-02-02 09:51:03 +01:00
Linus Rath a8c633e627 Update untracked consumption threshold to 1W (#29310) 2026-02-02 08:16:34 +00:00
Simon Lamon b659671814 Bump codespace to Python 3.14 (#29316) 2026-02-02 09:52:07 +02:00
dependabot[bot] 3060cdf355 Bump github/codeql-action from 4.31.11 to 4.32.0 (#29329)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.11 to 4.32.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/19b2f06db2b6f5108140aeb04014ef02b648f789...b20883b0cd1f46c72ae0ba6d1090936928f9fa30)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-02 08:44:06 +02:00
dependabot[bot] 215241df56 Bump actions/cache from 5.0.2 to 5.0.3 (#29328)
Bumps [actions/cache](https://github.com/actions/cache) from 5.0.2 to 5.0.3.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/8b402f58fbc84540c8b491a91e594a4576fec3d7...cdf6c1fa76f9f475f3d7449005a359c84ca0f306)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 5.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-02 08:43:49 +02:00
renovate[bot] f6852894b0 Update dependency @lokalise/node-api to v15.6.1 (#29322)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 06:33:24 +01:00
renovate[bot] 86d7205a3a Update dependency @braintree/sanitize-url to v7.1.2 (#29325)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 06:33:03 +01:00
Norbert Rittel 9c16ceda71 Also replace "Sensor type" with "Type of power measurement" (#29317)
Replace "Sensor type" with "Type of power measurement"
2026-02-01 10:44:47 +01:00
karwosts 288789a604 Use ha-form for condition template (#29301) 2026-02-01 10:18:27 +01:00
Norbert Rittel 09139d5bec Replace "Power sensor type" with "Type of power measurement" (#29305) 2026-02-01 09:54:56 +01:00
ildar170975 2d90be9af3 ha-config-device-page: fix placement for tooltip (#29302)
fix placement for tooltip
2026-02-01 09:39:08 +01:00
renovate[bot] 1c6464663e Update dependency @rsdoctor/rspack-plugin to v1.5.1 (#29297)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-01 09:37:46 +01:00
renovate[bot] 189f0b9472 Update dependency node-vibrant to v4.0.4 (#29293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-31 09:04:52 +02:00
renovate[bot] 771f5eaff4 Update dependency @vibrant/color to v4.0.4 (#29292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-31 08:49:36 +02:00
renovate[bot] a4cb3b5b01 Update dependency globals to v17.2.0 (#29288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 16:51:11 +00:00
Norbert Rittel eb1ae99a1f Replace "home page" with "Overview page" for preferences panel (#29284)
Replace "home page" with "Overview page"
2026-01-30 17:46:34 +01:00
Aidan Timson 655643eb3f Cleanup padding on matter dashboard expansion panel (#29286)
Cleanup padding on matter config expansion panel
2026-01-30 17:43:32 +01:00
Aidan Timson 02eb1e6832 Fix scrolling for labs page (#29287) 2026-01-30 17:41:40 +01:00
karwosts 62f7a2eea1 Fix areas cannot be deleted (#29285) 2026-01-30 14:01:31 +00:00
Aidan Timson a7f9b93018 Fix type error for missing hass.config race condition in themes mixin (#29280) 2026-01-30 12:54:08 +01:00
Paul Bottein 3e7011e2c8 Fix demo because of new default panel (#29279) 2026-01-30 11:48:47 +01:00
Aidan Timson 97f89bd983 Add missing settings nav items for quick search (#29278)
* Add missing repairs quick search item

* Add voice assistants
2026-01-30 12:11:09 +02:00
renovate[bot] 9aedaeabbf Update dependency @rspack/core to v1.7.4 (#29275)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 11:58:31 +02:00
Aidan Timson 7e839dc895 Change default shortcut tip in Quick Search to mod+k, add tip to settings (#29253) 2026-01-30 09:48:03 +00:00
Wendelin 1f6c916d11 Fix multi select in quick search (#29272)
Add item selection state management to QuickBar component
2026-01-30 10:14:32 +01:00
Wendelin 71bd12bb90 Fix --wa-color-text-normal (#29271)
Update normal text color variable in wa.globals.ts
2026-01-30 09:48:52 +02:00
Simon Lamon e741c14482 Remove duplicated text (#29265) 2026-01-30 08:46:18 +01:00
Wendelin a496448ed9 Fix device download diagnostic via overflow (#29269)
fix diagnostic download link handling to simplify URL signing
2026-01-30 09:42:40 +02:00
renovate[bot] c772285358 Update dependency typescript-eslint to v8.54.0 (#29267)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 08:13:50 +02:00
renovate[bot] 4fcdd09935 Update dependency @bundle-stats/plugin-webpack-filter to v4.21.9 (#29266)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 08:13:30 +02:00
Paul Bottein 157e89e5e7 Fix default lovelace yaml loading (#29240) 2026-01-29 23:26:39 +01:00
Wendelin c223f932f3 Implement fallback for dialog close event in Quick Search (#29260) 2026-01-29 17:41:12 +01:00
Wendelin fcd63e7cba Prevent quick search to close from hot keys (#29251) 2026-01-29 17:20:26 +01:00
Paul Bottein c1bab376c8 Remove default title for new dashboards (#29259) 2026-01-29 16:05:44 +00:00
Aidan Timson 01f5df6671 Add protocols to quick search (#29248)
Add protocols to quick search, extract logic and translations
2026-01-29 17:37:09 +02:00
TheJulianJES 241a655765 Fix Matter dashboard using disabled and ignored config entries (#29254) 2026-01-29 15:27:49 +00:00
Paul Bottein 9841a6341a Remove unused theme option in distribution card (#29250) 2026-01-29 15:26:46 +00:00
Paul Bottein 44c917b4b7 Prevent action in tile container (#29257) 2026-01-29 15:25:55 +00:00
uptimeZERO_ bff785ca68 Move theme settings to user settings (#29255) 2026-01-29 15:14:34 +00:00
Paul Bottein 1f15724024 Fix actions in dashboard overflow menu (#29256) 2026-01-29 15:46:10 +01:00
Paul Bottein ceaa3b8c17 Stop click propagation when clicking item in icon overflow (#29252) 2026-01-29 15:16:37 +01:00
Aidan Timson 7ed3ac1e24 Fixes for picker combo box scrolling and selection (#29242) 2026-01-29 15:11:30 +01:00
ildar170975 cfcb649a6f computeGroupEntitiesState(): fix condition (#29234)
* fix condition

* fix condition

* prettier
2026-01-29 15:35:53 +02:00
Aidan Timson ae036f4084 Remove unused "app" item from quick search (#29244) 2026-01-29 12:18:54 +00:00
Wendelin 6cca48e79d Fix quick search apps (#29238) 2026-01-29 11:30:09 +00:00
Paul Bottein feb9ce421d Fix default yaml lovelace panel loading (#29230) 2026-01-28 23:32:46 +01:00
Marcin Bauer 9639403865 Reorder profile settings to prioritize user preferences (#29202)
Reorganizes the User settings card on the profile page to show more
frequently used settings first:

- Default dashboard
- Customize sidebar
- Advanced mode (admin)
- Entity ID picker (admin)

Then adds a divider followed by localization settings:

- Language, number format, time format, date format, time zone, first weekday

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 14:56:45 -05:00
Aidan Timson 8e8cb095b1 Add meta+click/enter support to quick search (#29220)
* Allow meta+click event from combobox

* Handle new tab events for navigations

* Add mod+enter support for new tab

* Helper function
2026-01-28 18:52:15 +01:00
Paul Bottein 60079ce999 Add welcome banner for new overview dashboard (#29223) 2026-01-28 18:46:22 +01:00
Petar Petrov 4f3196adb9 Add non standard power sensor support (#28845)
* Add non standard power sensor support

* remove useless code

* GridPowerSourceInput type for grid power source saving
2026-01-28 18:20:35 +02:00
Bram Kragten 922e8c7752 Merge branch 'rc' into dev 2026-01-28 16:43:29 +01:00
Bram Kragten e63301cd9c Bumped version to 20260128.0 2026-01-28 16:42:45 +01:00
Aidan Timson 29a3d67e48 AI suggestions: Areas (#29090) 2026-01-28 16:35:24 +01:00
Bram Kragten 4c98a7791b Move developer tools panel to config panel (#29221) 2026-01-28 15:31:47 +00:00
Bram Kragten 5a76c3f606 Make addon selector an alias of app selector (#29222) 2026-01-28 15:27:30 +00:00
Steven Travers 251a4ce5ce Add device database labs feature (#29104)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2026-01-28 14:09:17 +01:00
karwosts 408735fa77 Live inline template previews (#27557)
* Live inline template previews

* Opt out for markdown, no fullscreen

* flask/flaskOff

* styling

* mdiBug

* Update src/components/ha-selector/ha-selector-template.ts

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* resub on connect

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-01-28 11:57:41 +02:00
Wendelin c0442b5b39 Fix automation sidebar ui supported check (#29219) 2026-01-28 10:52:12 +01:00
ildar170975 c6284987fd ha-filter-domains: set a plural caption (#29153)
* add "domains" / "caption"

* fix caption
2026-01-28 06:38:33 +01:00
Bram Kragten ed618124dc Fix double action handler area card (#29217) 2026-01-28 06:37:48 +01:00
Paul Bottein 3e350b7642 Attempt to make icon fetching and caching more reliable (#29195) 2026-01-27 21:34:19 +01:00
Paul Bottein c66b4e2027 Update top bar title margin on config and dashboard page (#29212) 2026-01-27 21:32:43 +01:00
Paul Bottein 4c25c639af Don't show back button when opening the add integration sub page directly (#29213) 2026-01-27 21:31:59 +01:00
Paul Bottein 0fbde5024e Fix back button in energy panel (#29214) 2026-01-27 21:31:05 +01:00
Paul Bottein b991a8122b Set home as default dashboard (#28446)
* Set home as default dashboard

* Handle lovelace to home redirect

* Remove special url path for lovelace

* Rename special rules for lovelace dashboard

* Update src/panels/lovelace/ha-panel-lovelace.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Handle default config

* Fix default section

* Early check for lovelace panel

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2026-01-27 17:08:19 +01:00
Jan Čermák c2c4e06915 Add AppSelector following the Add-ons->Apps rename (#29209) 2026-01-27 15:19:51 +00:00
Aidan Timson 91c12605d3 Add timezone selector (#29205)
* Add timezone selector

* Use timezone selector in clock card
2026-01-27 17:10:39 +02:00
Wendelin cddf91cfd0 Replace ha-md-button-menu with ha-dropdown (#29210) 2026-01-27 15:10:32 +00:00
Paul Bottein 6e1999ceb7 Update top bar background color with the same color as config panel (#29208) 2026-01-27 17:01:15 +02:00
Marcin Bauer 3b571d42fa Add find and replace button to code editor toolbar (#28980) 2026-01-27 14:53:50 +00:00
Marcin Bauer 08ee742233 Move entity attributes to three-dots menu subview (#29186)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 14:07:23 +00:00
Wendelin b3cc88e124 Fix PickerComboBox to not overwrite local selected section (#29206) 2026-01-27 13:41:45 +00:00
Paul Bottein 9fe9456f3c Add link to manage discovered devices in add integration dialog (#29188)
* Add link to manage discovered devices in add integration dialog

* Prettier

* Update src/panels/config/integrations/dialog-add-integration.ts

---------

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-01-27 14:04:33 +01:00
Wendelin 6d1d7690ef Refactor dropdown menus to use ha-dropdown and ha-dropdown-item components (#29204) 2026-01-27 12:36:53 +00:00
Aidan Timson 4a2b7324f7 Fix position of tooltips in sidebar (#29203) 2026-01-27 12:17:15 +00:00
Aidan Timson 15b85d6f19 Move developer tools to settings (admin) area (#29201) 2026-01-27 11:13:09 +00:00
ildar170975 c49115a91e ha-sidebar: exclude some items from scrollable + fade + various fixes (#28747)
Co-authored-by: Aidan Timson <aidan@timmo.dev>
2026-01-27 09:55:58 +00:00
Aidan Timson efc67a30f3 Migrate currency picker to generic (#29193)
* Migrate currency picker to generic

* Pass required pro
2026-01-27 11:54:28 +02:00
Aidan Timson bf41b3f7e3 Migrate country picker to generic picker (#29190)
* Migrate country picker to generic picker

* Add country code as secondary text

* Pass required prop

* Remove
2026-01-27 11:08:54 +02:00
Tomasz 30eb50a962 Add color setting for calendar entities (#28882)
* Enhance calendar entity options with color support and update UI components for color selection

* Add loading spinner to calendar components and improve event loading state management

* simplify

* Remove redundant color change check in HuiCalendarCard update logic

* Add color validation utility and update calendar components to use it. color need to be hex strings

* Adds logic to reset the _eventsLoaded state to false when either the card configuration or the entity registry changes, ensuring events are reloaded appropriately.

* remove casting

* Use SubscribeMixin for entity registry subscription

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-01-27 08:54:41 +00:00
Aidan Timson 567e8c51d0 Migrate timezone picker to generic (#29192)
* Migrate timezone picker to generic

* Pass required prop, remove query
2026-01-27 08:29:50 +02:00
renovate[bot] e214c79cd5 Update dependency vite-tsconfig-paths to v6.0.5 (#29198)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-27 08:24:18 +02:00
renovate[bot] c0cae1cead Update dependency @babel/helper-define-polyfill-provider to v0.6.6 (#29197)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-27 08:23:54 +02:00
Petar Petrov 22742eec84 Group small power consumers into Other node instead of hiding (#29185) 2026-01-26 17:05:41 +02:00
Aidan Timson 37d8273e7c Navigation picker: add sections/grouping and related nav paths (#29094)
* Add sections to navigation picker

* Use PANEL_DASHBOARDS to assign system dashboards to the dashboards section

* Clean

* Add context based related section

* Add integration icon for related device

* Add floor and sort

* Consolidate and cleanup

* Reuse type

* Add context check and catch findRelated errors

* Remove floor from set, use area

* Memoize related updates

* Log error

* Remove

* Fix icon path usage
2026-01-26 15:10:09 +02:00
AlCalzone 9ba34869be Display Z-Wave home ID as hexadecimal (#29187) 2026-01-26 11:58:09 +00:00
Petar Petrov 63284b328c Support app param in my links (#29142) 2026-01-26 08:55:42 +00:00
Simon Lamon 9bb9ae6ad6 Fix tooltip (#29169) 2026-01-26 08:44:45 +00:00
Petar Petrov 0377bf378d Fix sizing of the period selector in energy dashboard (#29183) 2026-01-26 08:42:03 +00:00
Kristel 7e5ecf4007 bugfix: correct assistant column and filtering for entities without unique id (#29039)
* correct assistant column and filtering for entities without unique id

* _fetchExposedEntities with additional guard

* refactor _getExposedEntitySettingsAsOptions for better code readability

* processed review comments

* move _fetchExposedEntities to firstUpdated

* resolve merge conflicts with #29137

* Update src/panels/config/entities/ha-config-entities.ts

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-01-26 07:27:11 +00:00
renovate[bot] e17055bef0 Update dependency globals to v17.1.0 (#29176)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-26 09:01:06 +02:00
renovate[bot] 38f64b0e93 Update vitest monorepo to v4.0.18 (#29175)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-26 09:00:43 +02:00
dependabot[bot] 4ea207d74a Bump release-drafter/release-drafter from 6.1.0 to 6.2.0 (#29180)
Bumps [release-drafter/release-drafter](https://github.com/release-drafter/release-drafter) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/release-drafter/release-drafter/releases)
- [Commits](https://github.com/release-drafter/release-drafter/compare/b1476f6e6eb133afa41ed8589daba6dc69b4d3f5...6db134d15f3909ccc9eefd369f02bd1e9cffdf97)

---
updated-dependencies:
- dependency-name: release-drafter/release-drafter
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-26 07:27:55 +01:00
dependabot[bot] 04b0db35f6 Bump actions/setup-python from 6.1.0 to 6.2.0 (#29182)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/83679a892e2d95755f2dac6acb0bfd1e9ac5d548...a309ff8b426b58ec0e2a45f0f869d46889d02405)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-26 07:27:39 +01:00
dependabot[bot] 0d22b88f27 Bump github/codeql-action from 4.31.10 to 4.31.11 (#29181)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.10 to 4.31.11.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/cdefb33c0f6224e58673d9004f47f7cb3e328b89...19b2f06db2b6f5108140aeb04014ef02b648f789)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.31.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-26 07:27:20 +01:00
dependabot[bot] ddf209bd8d Bump actions/checkout from 6.0.1 to 6.0.2 (#29179)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/8e8c483db84b4bee98b60c0593521ed34d9990e8...de0fac2e4500dabe0009e67214ff5f5447ce83dd)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-26 07:26:51 +01:00
renovate[bot] ce5c1d2a9f Update dependency @rspack/dev-server to v1.2.1 (#29178)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-26 07:26:37 +01:00
Simon Lamon 31168e1342 Cleanup unused hassio backup files (#29170)
Cleanup
2026-01-25 14:13:18 +02:00
renovate[bot] 5b5c671d89 Update dependency core-js to v3.48.0 (#29165)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-25 07:28:16 +02:00
renovate[bot] e6462835e5 Update dependency prettier to v3.8.1 (#29164)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-25 07:27:42 +02:00
renovate[bot] fa08a9801e Update dependency tar to v7.5.6 (#29154)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-24 07:45:27 +01:00
renovate[bot] c1d7100e91 Update dependency tar to v7.5.4 [SECURITY] (#29119)
* Update dependency tar to v7.5.4 [SECURITY]

* Update dependency tar to v7.5.4

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-01-24 05:42:13 +00:00
Paul Bottein 0f1ffaf5ac Remove blue color for header and reduce margin for dashboard view header (#29111)
* Use background color for header color

* Reduce margin above top title in home overview strategy

* Remove dark color (not needed anymore)
2026-01-23 17:42:38 +01:00
Petar Petrov 5ca8fd4095 Fix crash when using invalid visibility condition type (#29150) 2026-01-23 17:34:57 +01:00
renovate[bot] c3277ff8b2 Update dependency @rspack/core to v1.7.3 (#29147)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 17:29:10 +01:00
Petar Petrov aac463b34d Update minimum power threshold to 1 W in power sankey (#29148) 2026-01-23 17:28:42 +01:00
ildar170975 c8471cb623 Refactor processing values w/o unit in "ha-attribute-value" & "hui-attribute-row" (#28540)
* add "hideUnit" to formatEntityAttributeValue()

* add "hideUnit" to formatEntityAttributeValue()

* add "hideUnit" to computeAttributeValueDisplay()

* use formatEntityAttributeValue() with "hideUnit"

* fix logic for "hideUnit" for ha-attribute-value

* prettier

* remove hideUnit from formatEntityAttributeValue()

* revert to the initial code

* revert to the initial code

* revert to the initial code

* use formatEntityAttributeValuePart() to get a value w/o unit

* use formatEntityAttributeValueToParts() instead of formatEntityAttributeValuePart()

* fix a value

* fix name of a const

* Update src/components/ha-attribute-value.ts

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>

* Prettier

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2026-01-23 15:13:24 +00:00
ildar170975 bd33a94749 Add formatEntityAttributeValueToParts() function (and use for Entity card) (#28539) 2026-01-23 08:18:28 +01:00
Simon Lamon 6061f72f3a Fix cast (#29141) 2026-01-23 08:51:01 +02:00
Wendelin de85b08de4 Migrate ha-md-button-menu to ha-dropdown in 6 files (#29137)
Refactor dropdown menus to use ha-dropdown and ha-dropdown-item components

- Replaced ha-md-button-menu and related components with ha-dropdown and ha-dropdown-item in dialog-edit-sidebar, hass-tabs-subpage-data-table, ha-config-devices-dashboard, ha-config-entities.
- Updated event handling to accommodate new dropdown structure.
- Added wa-divider for better visual separation in dropdowns.
- Improved accessibility and usability of dropdown menus across various components.
2026-01-22 20:00:51 +01:00
Paul Bottein 2599804d22 Don't set icon slot in tile card if image url is set (#29140) 2026-01-22 19:23:42 +01:00
renovate[bot] 27e1fc9b91 Update dependency typescript-eslint to v8.53.1 (#29139)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 19:11:11 +01:00
ildar170975 ae627a9c66 Data tables: fix sorting for "Assistants" column (#29121)
fix sorting
2026-01-22 18:13:54 +01:00
renovate[bot] 12623c31da Update formatjs monorepo (#29138)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 18:13:28 +01:00
Petar Petrov 1ddbf4ba09 Add tap_action and image_tap_action to Area card (#29112)
* Add tap_action and image_tap_action to Area card

* limit actions
2026-01-22 16:05:54 +01:00
Paul Bottein af4d68e2b6 Use translation for media player source and sound mode in more info (#29135) 2026-01-22 14:59:27 +00:00
Wendelin 45b28d382c Remove ha-button-menu component (#29134) 2026-01-22 15:07:02 +01:00
karwosts be007399cc Prevent flashing the energy setup wizard when already configured (#29117) 2026-01-22 12:56:36 +00:00
Paul Bottein 823b4fc4f6 Remove color picker text color (#29133) 2026-01-22 13:49:04 +01:00
Bram Kragten 4e4882b9fa Remove supervisor build (#29132) 2026-01-22 13:30:33 +01:00
Paul Bottein 55c74d7959 Add empty state to Home panel strategies (#29113) 2026-01-22 12:15:27 +00:00
ildar170975 3231d46835 ha-label-picker: add color badges (#28977)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-01-22 13:04:17 +01:00
Wendelin 0d110cfc7e Migrate all ha-button-menu to ha-dropdown (#29129) 2026-01-22 11:57:37 +00:00
Paul Bottein 936c0cd6aa Open edit area dialog when clicking edit button in area view (#29128) 2026-01-22 11:35:36 +00:00
Wendelin 311cbad8fa Remove download file support checks and related code (#29124) 2026-01-22 12:02:51 +01:00
ildar170975 73a1ce90c3 Data tables: do not show "Assistants" column in "secondary" when narrow (#29120)
fix a column for "narrow"
2026-01-22 09:37:03 +02:00
uptimeZERO_ ea1b7b9dec Media player fixes (#29075)
* aligning ui of dialog and media bar

* refactored media progress logic to be reusable

* updating track times to be consistent with music assistant

* WIP aligning volume slider with music assistant

* migrating to ha-dropdown

* showing volume tooltip on touch devices

* Fixed volume slider going to 100 randomly

* Added scrolling support

* Refactored volume control logic
2026-01-21 20:28:32 +01:00
Paul Bottein fd506d4d72 Add assign area shortcut to home panel (#29082)
* Add assign area shortcut to home panel

* Set empty state for empty other devices page
2026-01-21 17:11:23 +01:00
karwosts a3be09018c Fix add entry button for integrations (#29106) 2026-01-21 16:13:03 +02:00
Wendelin 3364d4f578 Migrate button-menu components to dropdown in 7 files (#29105)
Migrate button-menu components to dropdown in various editors
2026-01-21 15:12:15 +02:00
Bram Kragten 956dbb5346 Merge branch 'rc' 2026-01-16 13:36:43 +01:00
Bram Kragten 2b7cd8fe3a Bumped version to 20260107.2 2026-01-16 13:36:27 +01:00
Wendelin dbff31a281 Automation add TCA: fix: prevent multiple dialog closures by tracking closing state (#28978) 2026-01-16 13:36:12 +01:00
Petar Petrov 6572200e8b Respect user-configured grid options for fixed_rows/fixed_columns cards (#28961) 2026-01-16 13:36:11 +01:00
Wendelin 1465515fb8 Fix category-picker unknown check (#28957) 2026-01-16 13:36:09 +01:00
Petar Petrov ddaba99f64 Sanitize names in history card and map card (#28947) 2026-01-16 13:36:08 +01:00
karwosts 1c73bc6f6c Update energy summary visibility condition (#28913)
* Update energy summary visibility condition

* add grid power as special case

* Always show summary when you have powersource
2026-01-16 13:36:07 +01:00
SmartCoder 44870cb3eb Fixed modal visibility issue in settings -> areas -> edit room (#28907)
* Fixed modal visibility issue in settings -> areas -> edit room

* converting both components to use ha-wa-dialog

* removed z-index from ha-wa-dialog

* fixed hardcoded .open in media browser dialog and remove unnecessary z-index CSS variables
2026-01-16 13:36:06 +01:00
Brendan Annable 481a90352b Fix timer restore bug (#28898) 2026-01-16 13:36:05 +01:00
Simon Lamon 9b536b2172 Remove twine and introduce trusted publishing (#27110)
* Remove twine and introduce trusted publishing

* Update release.yaml
2026-01-16 13:36:04 +01:00
Bram Kragten 78d41dfd55 Merge branch 'rc' 2026-01-09 23:28:00 +01:00
Bram Kragten 905435db3e Bumped version to 20260107.1 2026-01-09 23:26:47 +01:00
Yosi Levy ea73fd3f01 Fix for volume scroll in media player (#28891) 2026-01-09 23:26:22 +01:00
Yosi Levy e519a0203e Arrow fixes in media browser (#28890) 2026-01-09 23:26:21 +01:00
Bram Kragten d98ee7e0b5 Add support for choose selector to initial form data (#28876)
* Add support for choose selector to initial form data

* Update compute-initial-ha-form-data.ts
2026-01-09 23:26:20 +01:00
Bram Kragten 6fc8c17909 Fix color palette creation (#28867) 2026-01-09 23:26:19 +01:00
dcapslock 201169c3d8 Fix choose selector active_choice when card editor config changes (#28858) 2026-01-09 23:26:18 +01:00
DAccord 303538ac21 Handling empty history (#28852)
Co-authored-by: DAccord <11232265+DAccord@users.noreply.github.com>
2026-01-09 23:26:17 +01:00
Bram Kragten ac88f3ed0b Merge branch 'rc' 2026-01-07 16:32:25 +01:00
Bram Kragten 3c5a6193d0 Bumped version to 20260107.0 2026-01-07 16:32:09 +01:00
Wendelin 5ee4bd63f8 Fix logs provider picker mobile width (#28847) 2026-01-07 16:31:52 +01:00
Wendelin b193929bd9 Throttle unknown value checks in ha-generic-picker (#28842) 2026-01-07 16:31:51 +01:00
Paul Bottein 3bee5c8cd4 Remove ha-combo-box-textfield (#28841) 2026-01-07 16:31:50 +01:00
Paul Bottein 976c74b8da Prefill the field with current value when editing a custom text item (#28840) 2026-01-07 16:31:49 +01:00
Wendelin 3a4a13db21 Improve device picker performance (#28835) 2026-01-07 16:31:48 +01:00
Paul Bottein a2f033dd88 Reduce shadow effect for scrollable fade mixin (#28832) 2026-01-07 16:31:46 +01:00
Bram Kragten a44b94c8df Prevent showing error during loading of statistics picker (#28823) 2026-01-07 16:31:45 +01:00
Bram Kragten 8796830ff9 Bumped version to 20251229.1 2026-01-06 17:18:45 +01:00
Bram Kragten bdff13d5e1 Use target selector to filter references entities (#28822)
* Use target selector to filter references entities

* Update ha-selector-state.ts
2026-01-06 17:18:20 +01:00
Bram Kragten 4346484afc Use single path for thread icon, add KNX, simplify (#28819)
* Use single path for thread icon, simplify

* Add custom path for KNX
2026-01-06 17:18:19 +01:00
Bram Kragten 533694391e Remove iOS focus handling from dialogs (#28818) 2026-01-06 17:18:18 +01:00
Bram Kragten 3adba7aa1f Fix translation loading of choose selector (#28817) 2026-01-06 17:18:17 +01:00
karwosts b60552c025 Fix statistic-graph-card cutoff w/ energy date picker (#28810)
* Fix statistics-graph energy-date mode end-time with 5min statistics

* don't modify date/hour for 5minute graph

* suggestedMax use period instead of days

* go back to string types
2026-01-06 17:18:16 +01:00
Paul Bottein 3011d56101 Show close button when zwave firmware update is finished (#28805) 2026-01-06 17:18:15 +01:00
Aidan Timson c903c0d734 Add option for any state and show translated label for entity state values (#28803)
* Add option for any state

* Use translated labels for value
2026-01-06 17:18:14 +01:00
Aidan Timson 14be390994 Remove duplicate custom items, remove "no matching ..." when allow-custom-value set (#28801)
* Remove duplicate custom items, allow default from picker

* Memoize

* Memoize

* Memoize func

* Don't show no matching item when custom value is allowed

* Remove no items found label now unused

* Cleanup unused translations

* Restore used value

* Remove no items found label now unused

* Remove redundant comment

* Remove searchFn

* Ensure custom value isnt identical

* Fix duplicated value

* Fix duplicated value

* Use additional items for entity state content

* Fix duplicate values

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2026-01-06 17:18:12 +01:00
Paul Bottein d48019a48e Remove custom value for unknown icon in icon picker (#28800) 2026-01-06 17:18:11 +01:00
Copilot 7b5cbb76ef Display template targets with neutral badge instead of "Unknown area" error (#28799)
* Initial plan

* Add template target display with neutral badge

- Import mdiCodeBraces icon and isTemplate function
- Check if target ID is a template before checking if it exists
- Display grey {} icon with "Template" text for templated targets
- Add "template" translation key to target_summary
- Prevents misleading red "Unknown area" badge for template targets

Co-authored-by: piitaya <5878303+piitaya@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: piitaya <5878303+piitaya@users.noreply.github.com>
2026-01-06 17:18:10 +01:00
Paul Bottein c75fab025f Use regular item for bottom padding in combobox (#28798) 2026-01-06 17:18:09 +01:00
karwosts c007206fa0 Fix statistic names w/ energy_date_selection (#28787) 2026-01-06 17:18:08 +01:00
Norbert Rittel ab5b5a4276 A few small spelling fixes in user-facing strings (#28786)
- use correct spelling for "Wi-Fi" trademark
- capitalize "PIN" as abbreviation
- fix spelling of "set up" as verb
- fix sentence-casing
2026-01-06 17:18:07 +01:00
Paulus Schoutsen 9eb40f8470 Bluetooth panel to support multi adapter (#28763)
* Support multiple adapters in bluetooth panel

* Move connection allocations up

* Make it tabs

* Add icons

* Revert "Add icons"

This reverts commit e338b6e578.

* Revert "Make it tabs"

This reverts commit d1b19d5c3e.

* Fix scanner matching and no active connection slot support

* Update src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-01-06 17:18:06 +01:00
dependabot[bot] bc827d9bf1 Bump qs from 6.14.0 to 6.14.1 (#28760)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-06 17:18:05 +01:00
Simon Lamon 24f5d58691 Make cancel a secondary action in blueprint import (#28754) 2026-01-06 17:18:04 +01:00
Simon Lamon 13505a9104 Fix matter translations (#28752) 2026-01-06 17:18:03 +01:00
Simon Lamon c1d135aa16 Revert lit update (#28751) 2026-01-06 17:18:02 +01:00
Paulus Schoutsen 16d13c3202 Verify bluetooth config entries exist before showing entry (#28745) 2026-01-06 17:18:01 +01:00
Paulus Schoutsen 46b3c34ba1 Hide dashboard controls in kiosk mode (#28742) 2026-01-06 17:18:00 +01:00
Paulus Schoutsen 57a81b9de4 Add config entry picker for Z-Wave JS panel (#28741) 2026-01-06 17:17:58 +01:00
Simon Lamon 69f4f1dbed Provide kioskmode in demo (#28739) 2026-01-06 17:17:57 +01:00
Paulus Schoutsen 355a1aff3f Protocol link updates (#28736)
* Update icons Thread & Insteon

* Remove matter link

* Remove back path from ZHA

* Fix ZHA dashboard config entry
2026-01-06 17:17:56 +01:00
Franck Nijhof 3a3036c635 20251229.0 (#28727) 2025-12-29 14:34:15 +01:00
462 changed files with 10434 additions and 19397 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/devcontainers/python:1-3.13
FROM mcr.microsoft.com/devcontainers/python:1-3.14
ENV \
DEBIAN_FRONTEND=noninteractive \
+1 -1
View File
@@ -67,7 +67,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
<!--
If your issue is about how an entity is shown in the UI, please add the state
and attributes for all situations with a screenshot of the UI.
You can find this information at `/developer-tools/state`
You can find this information at `/config/developer-tools/state`
-->
```yaml
-5
View File
@@ -44,8 +44,3 @@ GitHub Actions:
- any-glob-to-any-file:
- .github/workflows/**
- .github/*.yml
Supervisor:
- changed-files:
- any-glob-to-any-file:
- hassio/src/**
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: dev
@@ -56,7 +56,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: master
+4 -28
View File
@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
@@ -37,7 +37,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
node_modules/.cache/prettier
@@ -58,7 +58,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
@@ -76,7 +76,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
@@ -101,27 +101,3 @@ jobs:
path: hass_frontend/
if-no-files-found: error
retention-days: 7
supervisor:
name: Build supervisor
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install --immutable
- name: Build Application
run: ./node_modules/.bin/gulp build-hassio
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: supervisor-bundle-stats
path: build/stats/*.json
if-no-files-found: error
+4 -4
View File
@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
+2 -2
View File
@@ -22,7 +22,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: dev
@@ -57,7 +57,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: master
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
+3 -3
View File
@@ -6,7 +6,7 @@ on:
- cron: "0 1 * * *"
env:
PYTHON_VERSION: "3.13"
PYTHON_VERSION: "3.14"
NODE_OPTIONS: --max_old_space_size=6144
permissions:
@@ -20,10 +20,10 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: ${{ env.PYTHON_VERSION }}
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
strategy:
matrix:
bundle: [frontend, supervisor]
bundle: [frontend]
build: [modern, legacy]
runs-on: ubuntu-latest
steps:
+1 -1
View File
@@ -18,6 +18,6 @@ jobs:
pull-requests: read
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6.1.0
- uses: release-drafter/release-drafter@6db134d15f3909ccc9eefd369f02bd1e9cffdf97 # v6.2.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+5 -34
View File
@@ -6,7 +6,7 @@ on:
- published
env:
PYTHON_VERSION: "3.13"
PYTHON_VERSION: "3.14"
NODE_OPTIONS: --max_old_space_size=6144
# Set default workflow permissions
@@ -26,10 +26,10 @@ jobs:
if: github.repository_owner == 'home-assistant'
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -84,7 +84,7 @@ jobs:
- name: Build wheels
uses: home-assistant/wheels@2025.12.0
with:
abi: cp313
abi: cp314
tag: musllinux_1_2
arch: amd64
wheels-key: ${{ secrets.WHEELS_KEY }}
@@ -98,7 +98,7 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
@@ -118,32 +118,3 @@ jobs:
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
release-supervisor:
name: Release supervisor frontend
if: github.event.release.prerelease == false
runs-on: ubuntu-latest
permissions:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install
- name: Download Translations
run: ./script/translations_download
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Build supervisor
run: hassio/script/build_hassio
- name: Tar folder
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
- name: Upload release asset
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Upload Translations
run: |
-55
View File
@@ -73,37 +73,6 @@
"instanceLimit": 1
}
},
{
"label": "Develop Supervisor panel",
"type": "gulp",
"task": "develop-hassio",
"problemMatcher": {
"owner": "ha-build",
"source": "ha-build",
"fileLocation": "absolute",
"severity": "error",
"pattern": [
{
"regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)",
"severity": 1,
"file": 2,
"message": 3,
"line": 4,
"column": 5
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "Changes detected. Starting compilation",
"endsPattern": "Build done @"
}
},
"isBackground": true,
"group": "build",
"runOptions": {
"instanceLimit": 1
}
},
{
"label": "Develop Gallery",
"type": "gulp",
@@ -246,20 +215,6 @@
"instanceLimit": 1
}
},
{
"label": "Run HA Core for Supervisor in devcontainer",
"type": "shell",
"command": "SUPERVISOR=${input:supervisorHost} SUPERVISOR_TOKEN=${input:supervisorToken} script/core",
"isBackground": true,
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [],
"runOptions": {
"instanceLimit": 1
}
},
{
"label": "Setup and fetch nightly translations",
"type": "gulp",
@@ -268,16 +223,6 @@
}
],
"inputs": [
{
"id": "supervisorHost",
"type": "promptString",
"description": "The IP of the Supervisor host running the Remote API proxy add-on"
},
{
"id": "supervisorToken",
"type": "promptString",
"description": "The token for the Remote API proxy add-on"
},
{
"id": "coreUrl",
"type": "promptString",
-1
View File
@@ -14,7 +14,6 @@ This is the repository for the official [Home Assistant](https://home-assistant.
- Development: [Instructions](https://developers.home-assistant.io/docs/frontend/development/)
- Production build: `script/build_frontend`
- Gallery: `cd gallery && script/develop_gallery`
- Supervisor: [Instructions](https://developers.home-assistant.io/docs/supervisor/developing)
## Frontend development
+4 -25
View File
@@ -18,14 +18,14 @@ module.exports.sourceMapURL = () => {
module.exports.ignorePackages = () => [];
// Files from NPM packages that we should replace with empty file
module.exports.emptyPackages = ({ isHassioBuild, isLandingPageBuild }) =>
module.exports.emptyPackages = ({ isLandingPageBuild }) =>
[
// Icons in supervisor conflict with icons in HA so we don't load.
(isHassioBuild || isLandingPageBuild) &&
// Icons in landingpage conflict with icons in HA so we don't load.
isLandingPageBuild &&
require.resolve(
path.resolve(paths.root_dir, "src/components/ha-icon.ts")
),
(isHassioBuild || isLandingPageBuild) &&
isLandingPageBuild &&
require.resolve(
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
),
@@ -36,7 +36,6 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
__BUILD__: JSON.stringify(latestBuild ? "modern" : "legacy"),
__VERSION__: JSON.stringify(env.version()),
__DEMO__: false,
__SUPERVISOR__: false,
__BACKWARDS_COMPAT__: false,
__STATIC_PATH__: "/static/",
__HASS_URL__: `\`${
@@ -289,26 +288,6 @@ module.exports.config = {
};
},
hassio({ isProdBuild, latestBuild, isStatsBuild, isTestBuild }) {
return {
name: "supervisor" + nameSuffix(latestBuild),
entry: {
entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.ts"),
},
outputPath: outputPath(paths.hassio_output_root, latestBuild),
publicPath: publicPath(latestBuild, paths.hassio_publicPath),
isProdBuild,
latestBuild,
isStatsBuild,
isTestBuild,
isHassioBuild: true,
defineOverlay: {
__SUPERVISOR__: true,
__STATIC_PATH__: `"${paths.hassio_publicPath}/static/"`,
},
};
},
gallery({ isProdBuild, latestBuild }) {
return {
name: "gallery" + nameSuffix(latestBuild),
-4
View File
@@ -24,10 +24,6 @@ gulp.task(
)
);
gulp.task("clean-hassio", async () =>
deleteSync([paths.hassio_output_root, paths.build_dir])
);
gulp.task(
"clean-gallery",
gulp.parallel("clean-translations", async () =>
-27
View File
@@ -43,29 +43,11 @@ const compressAppModernBrotli = () =>
const compressAppModernZopfli = () =>
compressModern(paths.app_output_root, paths.app_output_latest, "zopfli");
const compressHassioModernBrotli = () =>
compressModern(
paths.hassio_output_root,
paths.hassio_output_latest,
"brotli"
);
const compressHassioModernZopfli = () =>
compressModern(
paths.hassio_output_root,
paths.hassio_output_latest,
"zopfli"
);
const compressAppOtherBrotli = () =>
compressOther(paths.app_output_root, paths.app_output_latest, "brotli");
const compressAppOtherZopfli = () =>
compressOther(paths.app_output_root, paths.app_output_latest, "zopfli");
const compressHassioOtherBrotli = () =>
compressOther(paths.hassio_output_root, paths.hassio_output_latest, "brotli");
const compressHassioOtherZopfli = () =>
compressOther(paths.hassio_output_root, paths.hassio_output_latest, "zopfli");
gulp.task(
"compress-app",
gulp.parallel(
@@ -75,12 +57,3 @@ gulp.task(
compressAppOtherZopfli
)
);
gulp.task(
"compress-hassio",
gulp.parallel(
compressHassioModernBrotli,
compressHassioOtherBrotli,
compressHassioModernZopfli,
compressHassioOtherZopfli
)
);
-25
View File
@@ -266,28 +266,3 @@ gulp.task(
paths.landingPage_output_es5
)
);
const HASSIO_PAGE_ENTRIES = { "entrypoint.js": ["entrypoint"] };
gulp.task(
"gen-pages-hassio-dev",
genPagesDevTask(
HASSIO_PAGE_ENTRIES,
paths.hassio_dir,
paths.hassio_output_root,
"src",
paths.hassio_publicPath
)
);
gulp.task(
"gen-pages-hassio-prod",
genPagesProdTask(
HASSIO_PAGE_ENTRIES,
paths.hassio_dir,
paths.hassio_output_root,
paths.hassio_output_latest,
paths.hassio_output_es5,
"src"
)
);
-11
View File
@@ -123,22 +123,11 @@ gulp.task("copy-translations-app", async () => {
copyTranslations(staticDir);
});
gulp.task("copy-translations-supervisor", async () => {
const staticDir = paths.hassio_output_static;
copyTranslations(staticDir);
});
gulp.task("copy-translations-landing-page", async () => {
const staticDir = paths.landingPage_output_static;
copyTranslations(staticDir);
});
gulp.task("copy-static-supervisor", async () => {
const staticDir = paths.hassio_output_static;
copyLocaleData(staticDir);
copyFonts(staticDir);
});
gulp.task("copy-static-app", async () => {
const staticDir = paths.app_output_static;
// Basic static files
-45
View File
@@ -1,45 +0,0 @@
import gulp from "gulp";
import env from "../env.cjs";
import "./clean.js";
import "./compress.js";
import "./entry-html.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./translations.js";
import "./rspack.js";
gulp.task(
"develop-hassio",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "development";
},
"clean-hassio",
"gen-dummy-icons-json",
"gen-pages-hassio-dev",
"build-supervisor-translations",
"copy-translations-supervisor",
"build-locale-data",
"copy-static-supervisor",
"rspack-watch-hassio"
)
);
gulp.task(
"build-hassio",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
"clean-hassio",
"gen-dummy-icons-json",
"build-supervisor-translations",
"copy-translations-supervisor",
"build-locale-data",
"copy-static-supervisor",
"rspack-prod-hassio",
"gen-pages-hassio-prod",
...// Don't compress running tests
(env.isTestBuild() ? [] : ["compress-hassio"])
)
);
-1
View File
@@ -9,7 +9,6 @@ import "./fetch-nightly-translations.js";
import "./gallery.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./hassio.js";
import "./landing-page.js";
import "./locale-data.js";
import "./rspack.js";
-26
View File
@@ -13,7 +13,6 @@ import {
createCastConfig,
createDemoConfig,
createGalleryConfig,
createHassioConfig,
createLandingPageConfig,
} from "../rspack.cjs";
@@ -159,31 +158,6 @@ gulp.task("rspack-prod-cast", () =>
)
);
gulp.task("rspack-watch-hassio", () => {
// This command will run forever because we don't close compiler
rspack(
createHassioConfig({
isProdBuild: false,
latestBuild: true,
})
).watch({ ignored: /build/, poll: isWsl }, doneHandler());
gulp.watch(
path.join(paths.translations_src, "en.json"),
gulp.series("build-supervisor-translations", "copy-translations-supervisor")
);
});
gulp.task("rspack-prod-hassio", () =>
prodBuild(
bothBuilds(createHassioConfig, {
isProdBuild: true,
isStatsBuild: env.isStatsBuild(),
isTestBuild: env.isTestBuild(),
})
)
);
gulp.task("rspack-dev-server-gallery", () =>
runDevServer({
compiler: rspack(
+3 -13
View File
@@ -170,9 +170,7 @@ const setFragment = (fragment) => async () => {
};
const panelFragment = (fragment) =>
fragment !== "base" &&
fragment !== "supervisor" &&
fragment !== "landing-page";
fragment !== "base" && fragment !== "landing-page";
const HASHES = new Map();
@@ -207,18 +205,15 @@ const createTranslations = async () => {
FRAGMENTS.map((fragment) => {
switch (fragment) {
case "base":
// Remove the panels and supervisor to create the base translations
// Remove the panels and landing-page to create the base translations
return [
flatten({
...data,
ui: { ...data.ui, panel: undefined },
supervisor: undefined,
"landing-page": undefined,
}),
"",
];
case "supervisor":
// Supervisor key is at the top level
return [flatten(data.supervisor), ""];
case "landing-page":
// landing-page key is at the top level
return [flatten(data["landing-page"]), ""];
@@ -318,11 +313,6 @@ gulp.task(
)
);
gulp.task(
"build-supervisor-translations",
gulp.series(setFragment("supervisor"), "build-translations")
);
gulp.task(
"build-landing-page-translations",
gulp.series(setFragment("landing-page"), "build-translations")
-10
View File
@@ -49,15 +49,5 @@ module.exports = {
"../landing-page/dist/static"
),
hassio_dir: path.resolve(__dirname, "../hassio"),
hassio_output_root: path.resolve(__dirname, "../hassio/build"),
hassio_output_static: path.resolve(__dirname, "../hassio/build/static"),
hassio_output_latest: path.resolve(
__dirname,
"../hassio/build/frontend_latest"
),
hassio_output_es5: path.resolve(__dirname, "../hassio/build/frontend_es5"),
hassio_publicPath: "/api/hassio/app",
translations_src: path.resolve(__dirname, "../src/translations"),
};
+2 -23
View File
@@ -40,7 +40,6 @@ const createRspackConfig = ({
latestBuild,
isStatsBuild,
isTestBuild,
isHassioBuild,
isLandingPageBuild,
dontHash,
}) => {
@@ -168,13 +167,9 @@ const createRspackConfig = ({
);
},
}),
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).length
bundle.emptyPackages({ isLandingPageBuild }).length
? new rspack.NormalModuleReplacementPlugin(
new RegExp(
bundle
.emptyPackages({ isHassioBuild, isLandingPageBuild })
.join("|")
),
new RegExp(bundle.emptyPackages({ isLandingPageBuild }).join("|")),
path.resolve(paths.root_dir, "src/util/empty.js")
)
: false,
@@ -326,21 +321,6 @@ const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
const createCastConfig = ({ isProdBuild, latestBuild }) =>
createRspackConfig(bundle.config.cast({ isProdBuild, latestBuild }));
const createHassioConfig = ({
isProdBuild,
latestBuild,
isStatsBuild,
isTestBuild,
}) =>
createRspackConfig(
bundle.config.hassio({
isProdBuild,
latestBuild,
isStatsBuild,
isTestBuild,
})
);
const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
createRspackConfig(bundle.config.gallery({ isProdBuild, latestBuild }));
@@ -351,7 +331,6 @@ module.exports = {
createAppConfig,
createDemoConfig,
createCastConfig,
createHassioConfig,
createGalleryConfig,
createRspackConfig,
createLandingPageConfig,
+1 -1
View File
@@ -206,7 +206,7 @@ class HcCast extends LitElement {
}
private async _handlePickView(ev: CustomEvent<ActionDetail>) {
const path = this.lovelaceViews![ev.detail.index].path ?? ev.detail.index;
const path = this.lovelaceViews?.[ev.detail.index]?.path ?? ev.detail.index;
await ensureConnectedCastSession(this.castManager!, this.auth!);
castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path);
}
+6
View File
@@ -9,11 +9,14 @@ import { selectedDemoConfig } from "./configs/demo-configs";
import { mockAreaRegistry } from "./stubs/area_registry";
import { mockAuth } from "./stubs/auth";
import { mockConfigEntries } from "./stubs/config_entries";
import { mockDeviceRegistry } from "./stubs/device_registry";
import { mockEnergy } from "./stubs/energy";
import { energyEntities } from "./stubs/entities";
import { mockEntityRegistry } from "./stubs/entity_registry";
import { mockEvents } from "./stubs/events";
import { mockFloorRegistry } from "./stubs/floor_registry";
import { mockFrontend } from "./stubs/frontend";
import { mockLabelRegistry } from "./stubs/label_registry";
import { mockIcons } from "./stubs/icons";
import { mockHistory } from "./stubs/history";
import { mockLovelace } from "./stubs/lovelace";
@@ -60,6 +63,9 @@ export class HaDemo extends HomeAssistantAppEl {
mockPersistentNotification(hass);
mockConfigEntries(hass);
mockAreaRegistry(hass);
mockDeviceRegistry(hass);
mockFloorRegistry(hass);
mockLabelRegistry(hass);
mockEntityRegistry(hass, [
{
config_entry_id: "co2signal",
+21
View File
@@ -27,4 +27,25 @@ export const mockFrontend = (hass: MockHomeAssistant) => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
return () => {};
});
hass.mockWS(
"frontend/subscribe_system_data",
(_msg, currentHass, onChange) => {
onChange?.({
value: currentHass.systemData,
});
// eslint-disable-next-line @typescript-eslint/no-empty-function
return () => {};
}
);
hass.mockWS("labs/subscribe", (_msg, _currentHass, onChange) => {
onChange?.({
preview_feature: _msg.preview_feature,
domain: _msg.domain,
enabled: false,
is_built_in: true,
});
// eslint-disable-next-line @typescript-eslint/no-empty-function
return () => {};
});
hass.mockWS("repairs/list_issues", () => ({ issues: [] }));
};
+11 -1
View File
@@ -7,8 +7,18 @@ export const mockTemplate = (hass: MockHomeAssistant) => {
})
);
hass.mockWS("render_template", (msg, _hass, onChange) => {
let result = msg.template;
// Simple variable substitution for demo purposes
if (msg.variables) {
for (const [key, value] of Object.entries(msg.variables)) {
result = result.replace(
new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, "g"),
String(value)
);
}
}
onChange!({
result: msg.template,
result,
listeners: { all: false, domains: [], entities: [], time: false },
});
// eslint-disable-next-line @typescript-eslint/no-empty-function
-1
View File
@@ -43,7 +43,6 @@ export default tseslint.config(
__BUILD__: false,
__VERSION__: false,
__STATIC_PATH__: false,
__SUPERVISOR__: false,
},
parser: tseslint.parser,
+2 -2
View File
@@ -169,7 +169,7 @@ const SCHEMAS: {
{
title: "Selectors",
translations: {
addon: "App",
app: "App",
entity: "Entity",
device: "Device",
area: "Area",
@@ -188,7 +188,7 @@ const SCHEMAS: {
entities: "Entities",
},
schema: [
{ name: "addon", selector: { addon: {} } },
{ name: "app", selector: { app: {} } },
{ name: "entity", selector: { entity: {} } },
{
name: "Attribute",
+1 -1
View File
@@ -239,7 +239,7 @@ const SCHEMAS: {
selector: { config_entry: {} },
},
duration: { name: "Duration", selector: { duration: {} } },
addon: { name: "App", selector: { addon: {} } },
app: { name: "App", selector: { app: {} } },
number_box: {
name: "Number Box",
selector: {
-9
View File
@@ -1,9 +0,0 @@
const path = require("path");
module.exports = {
// Target directory for the build.
buildDir: path.resolve(__dirname, "build"),
nodeDir: path.resolve(__dirname, "../node_modules"),
// Path where the Hass.io frontend will be publicly available.
publicPath: "/api/hassio/app",
};
-9
View File
@@ -1,9 +0,0 @@
#!/bin/sh
# Builds the Hass.io app for production
# Stop on errors
set -e
cd "$(dirname "$0")/../.."
./node_modules/.bin/gulp build-hassio
-9
View File
@@ -1,9 +0,0 @@
#!/bin/sh
# Run the Hass.io development server
# Stop on errors
set -e
cd "$(dirname "$0")/../.."
./node_modules/.bin/gulp develop-hassio
@@ -1,149 +0,0 @@
import { mdiArrowUpBoldCircle, mdiPuzzle } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { navigate } from "../../../src/common/navigate";
import { caseInsensitiveStringCompare } from "../../../src/common/string/compare";
import "../../../src/components/ha-card";
import type { HassioAddonRepository } from "../../../src/data/hassio/addon";
import type { StoreAddon } from "../../../src/data/supervisor/store";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import type { HomeAssistant } from "../../../src/types";
import "../components/hassio-card-content";
import { filterAndSort } from "../components/hassio-filter-addons";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-addon-repository")
export class HassioAddonRepositoryEl extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public repo!: HassioAddonRepository;
@property({ attribute: false }) public addons!: StoreAddon[];
@property() public filter!: string;
private _getAddons = memoizeOne((addons: StoreAddon[], filter?: string) => {
if (filter) {
return filterAndSort(addons, filter);
}
return addons.sort((a, b) =>
caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language)
);
});
protected render(): TemplateResult {
const repo = this.repo;
let _addons = this.addons;
if (!this.hass.userData?.showAdvanced) {
_addons = _addons.filter(
(addon) => !addon.advanced && addon.stage === "stable"
);
}
const addons = this._getAddons(_addons, this.filter);
if (this.filter && addons.length < 1) {
return html`
<div class="content">
<p class="description">
${this.supervisor.localize("store.no_results_found", {
repository: repo.name,
})}
</p>
</div>
`;
}
return html`
<div class="content">
<h1>${repo.name}</h1>
<div class="card-group">
${addons.map(
(addon) => html`
<ha-card
outlined
.addon=${addon}
class=${addon.available ? "" : "not_available"}
@click=${this._addonTapped}
>
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
.title=${addon.name}
.description=${addon.description}
.available=${addon.available}
.icon=${addon.installed && addon.update_available
? mdiArrowUpBoldCircle
: mdiPuzzle}
.iconTitle=${addon.installed
? addon.update_available
? this.supervisor.localize(
"common.new_version_available"
)
: this.supervisor.localize("app.state.installed")
: addon.available
? this.supervisor.localize("app.state.not_installed")
: this.supervisor.localize("app.state.not_available")}
.iconClass=${addon.installed
? addon.update_available
? "update"
: "installed"
: !addon.available
? "not_available"
: ""}
.iconImage=${atLeastVersion(
this.hass.config.version,
0,
105
) && addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined}
.showTopbar=${addon.installed || !addon.available}
.topbarClass=${addon.installed
? addon.update_available
? "update"
: "installed"
: !addon.available
? "unavailable"
: ""}
></hassio-card-content>
</div>
</ha-card>
`
)}
</div>
</div>
`;
}
private _addonTapped(ev) {
navigate(`/hassio/addon/${ev.currentTarget.addon.slug}?store=true`);
}
static get styles(): CSSResultGroup {
return [
hassioStyle,
css`
ha-card {
cursor: pointer;
overflow: hidden;
}
.not_available {
opacity: 0.6;
}
a.repo {
color: var(--primary-text-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-repository": HassioAddonRepositoryEl;
}
}
@@ -1,248 +0,0 @@
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import { mdiDotsVertical } from "@mdi/js";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { navigate } from "../../../src/common/navigate";
import { extractSearchParam } from "../../../src/common/url/search-params";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-list-item";
import "../../../src/components/search-input";
import type { HassioAddonRepository } from "../../../src/data/hassio/addon";
import { reloadHassioAddons } from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import type { StoreAddon } from "../../../src/data/supervisor/store";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
import type { HomeAssistant, Route } from "../../../src/types";
import { showRegistriesDialog } from "../dialogs/registries/show-dialog-registries";
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
import "./hassio-addon-repository";
const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
if (a.slug === "local") {
return -1;
}
if (b.slug === "local") {
return 1;
}
if (a.slug === "core") {
return -1;
}
if (b.slug === "core") {
return 1;
}
return a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1;
};
@customElement("hassio-addon-store")
export class HassioAddonStore extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@state() private _filter?: string;
public async refreshData() {
try {
await reloadHassioAddons(this.hass);
} catch (err) {
showAlertDialog(this, {
text: extractApiErrorMessage(err),
});
} finally {
this._loadData();
}
}
protected render() {
let repos: (TemplateResult | typeof nothing)[] = [];
if (this.supervisor.store.repositories) {
repos = this.addonRepositories(
this.supervisor.store.repositories,
this.supervisor.store.addons,
this._filter
);
}
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.header=${this.supervisor.localize("panel.store")}
>
<ha-button-menu slot="toolbar-icon" @action=${this._handleAction}>
<ha-icon-button
.label=${this.supervisor.localize("common.menu")}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<ha-list-item>
${this.supervisor.localize("store.check_updates")}
</ha-list-item>
<ha-list-item>
${this.supervisor.localize("store.repositories")}
</ha-list-item>
${this.hass.userData?.showAdvanced &&
atLeastVersion(this.hass.config.version, 0, 117)
? html`<ha-list-item>
${this.supervisor.localize("store.registries")}
</ha-list-item>`
: ""}
</ha-button-menu>
${repos.length === 0
? html`<hass-loading-screen no-toolbar></hass-loading-screen>`
: html`
<div class="search">
<search-input
.hass=${this.hass}
.filter=${this._filter}
@value-changed=${this._filterChanged}
></search-input>
</div>
${repos}
`}
${!this.hass.userData?.showAdvanced
? html`
<div class="advanced">
<a href="/profile" target="_top">
${this.supervisor.localize("store.missing_apps")}
</a>
</div>
`
: ""}
</hass-subpage>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
const repositoryUrl = extractSearchParam("repository_url");
navigate("/hassio/store", { replace: true });
if (repositoryUrl) {
this._manageRepositories(repositoryUrl);
}
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
this._loadData();
}
private addonRepositories = memoizeOne(
(
repositories: HassioAddonRepository[],
addons: StoreAddon[],
filter?: string
) =>
repositories.sort(sortRepos).map((repo) => {
const filteredAddons = addons.filter(
(addon) => addon.repository === repo.slug
);
return filteredAddons.length !== 0
? html`
<hassio-addon-repository
.hass=${this.hass}
.repo=${repo}
.addons=${filteredAddons}
.filter=${filter!}
.supervisor=${this.supervisor}
></hassio-addon-repository>
`
: nothing;
})
);
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this.refreshData();
break;
case 1:
this._manageRepositoriesClicked();
break;
case 2:
this._manageRegistries();
break;
}
}
private _apiCalled(ev) {
if (ev.detail.success) {
this._loadData();
}
}
private _manageRepositoriesClicked() {
this._manageRepositories();
}
private _manageRepositories(url?: string) {
showRepositoriesDialog(this, {
supervisor: this.supervisor,
url,
});
}
private _manageRegistries() {
showRegistriesDialog(this, { supervisor: this.supervisor });
}
private _loadData() {
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",
});
}
private _filterChanged(e) {
this._filter = e.detail.value;
}
static styles = css`
hassio-addon-repository {
margin-top: 24px;
}
.search {
position: sticky;
top: 0;
z-index: 2;
}
search-input {
display: block;
--mdc-text-field-fill-color: var(--sidebar-background-color);
--mdc-text-field-idle-line-color: var(--divider-color);
}
.advanced {
padding: 12px;
display: flex;
flex-wrap: wrap;
color: var(--primary-text-color);
}
.advanced a {
margin-left: 0.5em;
margin-inline-start: 0.5em;
margin-inline-end: initial;
color: var(--primary-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-store": HassioAddonStore;
}
}
@@ -1,207 +0,0 @@
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { stopPropagation } from "../../../../src/common/dom/stop_propagation";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-list-item";
import "../../../../src/components/ha-select";
import type {
HassioAddonDetails,
HassioAddonSetOptionParams,
} from "../../../../src/data/hassio/addon";
import { setHassioAddonOption } from "../../../../src/data/hassio/addon";
import type { HassioHardwareAudioDevice } from "../../../../src/data/hassio/hardware";
import { fetchHassioHardwareAudio } from "../../../../src/data/hassio/hardware";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
import { hassioStyle } from "../../resources/hassio-style";
@customElement("hassio-addon-audio")
class HassioAddonAudio extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@property({ type: Boolean }) public disabled = false;
@state() private _error?: string;
@state() private _inputDevices?: HassioHardwareAudioDevice[];
@state() private _outputDevices?: HassioHardwareAudioDevice[];
@state() private _selectedInput!: null | string;
@state() private _selectedOutput!: null | string;
protected render(): TemplateResult {
return html`
<ha-card
outlined
.header=${this.supervisor.localize("app.configuration.audio.header")}
>
<div class="card-content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
${this._inputDevices &&
html`<ha-select
.label=${this.supervisor.localize("app.configuration.audio.input")}
@selected=${this._setInputDevice}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._selectedInput!}
.disabled=${this.disabled}
>
${this._inputDevices.map(
(item) => html`
<ha-list-item .value=${item.device || ""}>
${item.name}
</ha-list-item>
`
)}
</ha-select>`}
${this._outputDevices &&
html`<ha-select
.label=${this.supervisor.localize("app.configuration.audio.output")}
@selected=${this._setOutputDevice}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._selectedOutput!}
.disabled=${this.disabled}
>
${this._outputDevices.map(
(item) => html`
<ha-list-item .value=${item.device || ""}
>${item.name}</ha-list-item
>
`
)}
</ha-select>`}
</div>
<div class="card-actions">
<ha-progress-button
.disabled=${this.disabled}
@click=${this._saveSettings}
>
${this.supervisor.localize("common.save")}
</ha-progress-button>
</div>
</ha-card>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
:host,
ha-card {
display: block;
}
.card-actions {
text-align: right;
}
ha-select {
width: 100%;
}
ha-select:last-child {
margin-top: 8px;
}
`,
];
}
protected willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (changedProperties.has("addon")) {
this._addonChanged();
}
}
private _setInputDevice(ev): void {
const device = ev.target.value;
this._selectedInput = device;
}
private _setOutputDevice(ev): void {
const device = ev.target.value;
this._selectedOutput = device;
}
private async _addonChanged(): Promise<void> {
this._selectedInput =
this.addon.audio_input === null ? "default" : this.addon.audio_input;
this._selectedOutput =
this.addon.audio_output === null ? "default" : this.addon.audio_output;
if (this._outputDevices) {
return;
}
const noDevice: HassioHardwareAudioDevice = {
device: "default",
name: this.supervisor.localize("app.configuration.audio.default"),
};
try {
const { audio } = await fetchHassioHardwareAudio(this.hass);
const input = Object.keys(audio.input).map((key) => ({
device: key,
name: audio.input[key],
}));
const output = Object.keys(audio.output).map((key) => ({
device: key,
name: audio.output[key],
}));
this._inputDevices = [noDevice, ...input];
this._outputDevices = [noDevice, ...output];
} catch {
this._error = "Failed to fetch audio hardware";
this._inputDevices = [noDevice];
this._outputDevices = [noDevice];
}
}
private async _saveSettings(ev: CustomEvent): Promise<void> {
if (this.disabled) {
return;
}
const button = ev.currentTarget as any;
button.progress = true;
this._error = undefined;
const data: HassioAddonSetOptionParams = {
audio_input:
this._selectedInput === "default" ? null : this._selectedInput,
audio_output:
this._selectedOutput === "default" ? null : this._selectedOutput,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
}
} catch {
this._error = "Failed to set addon audio device";
}
button.progress = false;
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-audio": HassioAddonAudio;
}
}
@@ -1,113 +0,0 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../src/components/ha-spinner";
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
import "../info/hassio-addon-system-managed";
import "./hassio-addon-audio";
import "./hassio-addon-config";
import "./hassio-addon-network";
@customElement("hassio-addon-config-tab")
class HassioAddonConfigDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon?: HassioAddonDetails;
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, attribute: "control-enabled" })
public controlEnabled = false;
protected render(): TemplateResult {
if (!this.addon) {
return html`<ha-spinner></ha-spinner>`;
}
const hasConfiguration =
(this.addon.options && Object.keys(this.addon.options).length) ||
(this.addon.schema && Object.keys(this.addon.schema).length);
return html`
<div class="content">
${this.addon.system_managed &&
(hasConfiguration || this.addon.network || this.addon.audio)
? html`
<hassio-addon-system-managed
.supervisor=${this.supervisor}
.narrow=${this.narrow}
.hideButton=${this.controlEnabled}
></hassio-addon-system-managed>
`
: nothing}
${hasConfiguration || this.addon.network || this.addon.audio
? html`
${hasConfiguration
? html`
<hassio-addon-config
.hass=${this.hass}
.addon=${this.addon}
.supervisor=${this.supervisor}
.disabled=${this.addon.system_managed &&
!this.controlEnabled}
></hassio-addon-config>
`
: nothing}
${this.addon.network
? html`
<hassio-addon-network
.hass=${this.hass}
.addon=${this.addon}
.supervisor=${this.supervisor}
.disabled=${this.addon.system_managed &&
!this.controlEnabled}
></hassio-addon-network>
`
: nothing}
${this.addon.audio
? html`
<hassio-addon-audio
.hass=${this.hass}
.addon=${this.addon}
.supervisor=${this.supervisor}
.disabled=${this.addon.system_managed &&
!this.controlEnabled}
></hassio-addon-audio>
`
: nothing}
`
: this.supervisor.localize("app.configuration.no_configuration")}
</div>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
.content {
margin: auto;
padding: 8px;
max-width: 1024px;
}
hassio-addon-network,
hassio-addon-audio,
hassio-addon-config {
margin-bottom: 24px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-config-tab": HassioAddonConfigDashboard;
}
}
@@ -1,507 +0,0 @@
import type { ActionDetail } from "@material/mwc-list";
import { mdiDotsVertical } from "@mdi/js";
import { DEFAULT_SCHEMA, Type } from "js-yaml";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button-menu";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-form/ha-form";
import type {
HaFormSchema,
HaFormDataContainer,
} from "../../../../src/components/ha-form/types";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-list-item";
import "../../../../src/components/ha-switch";
import "../../../../src/components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor";
import type {
HassioAddonDetails,
HassioAddonSetOptionParams,
} from "../../../../src/data/hassio/addon";
import {
setHassioAddonOption,
validateHassioAddonOption,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
import { hassioStyle } from "../../resources/hassio-style";
import type { ObjectSelector, Selector } from "../../../../src/data/selector";
const SUPPORTED_UI_TYPES = [
"string",
"select",
"boolean",
"integer",
"float",
"schema",
];
const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([
new Type("!secret", {
kind: "scalar",
construct: (data) => `!secret ${data}`,
}),
]);
const MASKED_FIELDS = ["password", "secret", "token"];
@customElement("hassio-addon-config")
class HassioAddonConfig extends LitElement {
@property({ attribute: false }) public addon!: HassioAddonDetails;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public disabled = false;
@state() private _configHasChanged = false;
@state() private _valid = true;
@state() private _canShowSchema = false;
@state() private _showOptional = false;
@state() private _error?: string;
@state() private _options?: Record<string, unknown>;
@state() private _yamlMode = false;
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
private _getTranslationEntry(
language: string,
entry: HaFormSchema,
options?: { path?: string[] }
) {
let parent = this.addon.translations[language]?.configuration;
if (!parent) return undefined;
if (options?.path) {
for (const key of options.path) {
parent = parent[key]?.fields;
if (!parent) return undefined;
}
}
return parent[entry.name];
}
public computeLabel = (
entry: HaFormSchema,
_data: HaFormDataContainer,
options?: { path?: string[] }
): string =>
this._getTranslationEntry(this.hass.language, entry, options)?.name ||
this._getTranslationEntry("en", entry, options)?.name ||
entry.name;
public computeHelper = (
entry: HaFormSchema,
options?: { path?: string[] }
): string =>
this._getTranslationEntry(this.hass.language, entry, options)
?.description ||
this._getTranslationEntry("en", entry, options)?.description ||
"";
private _convertSchema = memoizeOne(
// Convert supervisor schema to selectors
(schema: readonly HaFormSchema[]): HaFormSchema[] =>
this._convertSchemaElements(schema)
);
private _convertSchemaElements(
schema: readonly HaFormSchema[]
): HaFormSchema[] {
return schema.map((entry) => this._convertSchemaElement(entry));
}
private _convertSchemaElement(entry: any): HaFormSchema {
if (entry.type === "schema" && !entry.multiple) {
return {
name: entry.name,
type: "expandable",
required: entry.required,
schema: this._convertSchemaElements(entry.schema),
};
}
const selector = this._convertSchemaElementToSelector(entry, false);
if (selector) {
return {
name: entry.name,
required: entry.required,
selector,
};
}
return entry;
}
private _convertSchemaElementToSelector(
entry: any,
force: boolean
): Selector | null {
if (entry.type === "select") {
return { select: { options: entry.options } };
}
if (entry.type === "string") {
return entry.multiple
? { select: { options: [], multiple: true, custom_value: true } }
: {
text: {
type: entry.format
? entry.format
: MASKED_FIELDS.includes(entry.name)
? "password"
: "text",
},
};
}
if (entry.type === "boolean") {
return { boolean: {} };
}
if (entry.type === "schema") {
const fields: NonNullable<ObjectSelector["object"]>["fields"] = {};
for (const child_entry of entry.schema) {
fields[child_entry.name] = {
required: child_entry.required,
selector: this._convertSchemaElementToSelector(child_entry, true)!,
};
}
return {
object: {
multiple: entry.multiple,
fields,
},
};
}
if (entry.type === "float" || entry.type === "integer") {
return {
number: {
mode: "box",
step: entry.type === "float" ? "any" : undefined,
},
};
}
if (force) {
return { object: {} };
}
return null;
}
private _filteredSchema = memoizeOne(
(options: Record<string, unknown>, schema: HaFormSchema[]) =>
schema.filter((entry) => entry.name in options || entry.required)
);
protected render(): TemplateResult {
const showForm =
!this._yamlMode && this._canShowSchema && this.addon.schema;
const hasHiddenOptions =
showForm &&
JSON.stringify(this.addon.schema) !==
JSON.stringify(
this._filteredSchema(this.addon.options, this.addon.schema!)
);
return html`
<h1>${this.addon.name}</h1>
<ha-card outlined>
<div class="header">
<h2>
${this.supervisor.localize("app.configuration.options.header")}
</h2>
<div class="card-menu">
<ha-button-menu @action=${this._handleAction}>
<ha-icon-button
.label=${this.supervisor.localize("common.menu")}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<ha-list-item .disabled=${!this._canShowSchema || this.disabled}>
${this._yamlMode
? this.supervisor.localize(
"app.configuration.options.edit_in_ui"
)
: this.supervisor.localize(
"app.configuration.options.edit_in_yaml"
)}
</ha-list-item>
<ha-list-item
class=${!this.disabled ? "warning" : ""}
.disabled=${this.disabled}
>
${this.supervisor.localize("common.reset_defaults")}
</ha-list-item>
</ha-button-menu>
</div>
</div>
<div class="card-content">
${showForm
? html`<ha-form
.hass=${this.hass}
.disabled=${this.disabled}
.data=${this._options!}
@value-changed=${this._configChanged}
.computeLabel=${this.computeLabel}
.computeHelper=${this.computeHelper}
.schema=${this._convertSchema(
this._showOptional
? this.addon.schema!
: this._filteredSchema(
this.addon.options,
this.addon.schema!
)
)}
></ha-form>`
: html`<ha-yaml-editor
@value-changed=${this._configChanged}
.yamlSchema=${ADDON_YAML_SCHEMA}
></ha-yaml-editor>`}
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${!this._yamlMode ||
(this._canShowSchema && this.addon.schema) ||
this._valid
? ""
: html`
<ha-alert alert-type="error">
${this.supervisor.localize(
"app.configuration.options.invalid_yaml"
)}
</ha-alert>
`}
</div>
${hasHiddenOptions
? html`<ha-formfield
class="show-additional"
.label=${this.supervisor.localize(
"app.configuration.options.show_unused_optional"
)}
>
<ha-switch
@change=${this._toggleOptional}
.checked=${this._showOptional}
>
</ha-switch>
</ha-formfield>`
: ""}
<div class="card-actions right">
<ha-progress-button
@click=${this._saveTapped}
.disabled=${this.disabled ||
!this._configHasChanged ||
!this._valid}
>
${this.supervisor.localize("common.save")}
</ha-progress-button>
</div>
</ha-card>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._canShowSchema = !this.addon.schema!.find(
(entry) =>
// @ts-ignore
!SUPPORTED_UI_TYPES.includes(entry.type)
);
this._yamlMode = !this._canShowSchema;
}
protected updated(changedProperties: PropertyValues): void {
if (changedProperties.has("addon")) {
this._options = { ...this.addon.options };
}
super.updated(changedProperties);
if (
changedProperties.has("_yamlMode") ||
changedProperties.has("_options")
) {
if (this._yamlMode) {
const editor = this._editor;
if (editor) {
editor.setValue(this._options!);
}
}
}
}
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._yamlMode = !this._yamlMode;
break;
case 1:
this._resetTapped(ev);
break;
}
}
private _toggleOptional() {
this._showOptional = !this._showOptional;
}
private _configChanged(ev): void {
if (this.addon.schema && this._canShowSchema && !this._yamlMode) {
this._valid = true;
this._configHasChanged = true;
this._options = ev.detail.value;
} else {
this._configHasChanged = true;
this._valid = ev.detail.isValid;
}
}
private async _resetTapped(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize("confirm.reset_options.title"),
text: this.supervisor.localize("confirm.reset_options.text"),
confirmText: this.supervisor.localize("common.reset_options"),
dismissText: this.supervisor.localize("common.cancel"),
destructive: true,
});
if (!confirmed) {
button.progress = false;
return;
}
this._error = undefined;
const data: HassioAddonSetOptionParams = {
options: null,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
this._configHasChanged = false;
const eventdata = {
success: true,
response: undefined,
path: "options",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
this._error = this.supervisor.localize("app.failed_to_reset", {
error: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _saveTapped(ev: CustomEvent): Promise<void> {
if (this.disabled || !this._configHasChanged || !this._valid) {
return;
}
const button = ev.currentTarget as any;
const options: Record<string, unknown> = this._yamlMode
? this._editor?.value
: this._options;
const eventdata = {
success: true,
response: undefined,
path: "options",
};
button.progress = true;
this._error = undefined;
try {
const validation = await validateHassioAddonOption(
this.hass,
this.addon.slug,
options
);
if (!validation.valid) {
throw Error(validation.message);
}
await setHassioAddonOption(this.hass, this.addon.slug, {
options,
});
this._configHasChanged = false;
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
}
} catch (err: any) {
this._error = this.supervisor.localize("app.failed_to_save", {
error: extractApiErrorMessage(err),
});
eventdata.success = false;
}
button.progress = false;
fireEvent(this, "hass-api-called", eventdata);
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
:host {
display: block;
}
ha-card {
display: block;
}
.card-actions {
display: flex;
justify-content: space-between;
}
.card-menu {
float: right;
z-index: 3;
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
ha-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
.header {
display: flex;
justify-content: space-between;
}
.header h2 {
color: var(--ha-card-header-color, var(--primary-text-color));
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
letter-spacing: -0.012em;
line-height: var(--ha-line-height-expanded);
padding: 12px 16px 16px;
display: block;
margin-block: 0px;
font-weight: var(--ha-font-weight-normal);
}
.card-actions.right {
justify-content: flex-end;
}
.show-additional {
padding: 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-config": HassioAddonConfig;
}
}
@@ -1,264 +0,0 @@
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import type {
HassioAddonDetails,
HassioAddonSetOptionParams,
} from "../../../../src/data/hassio/addon";
import { setHassioAddonOption } from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
import { hassioStyle } from "../../resources/hassio-style";
@customElement("hassio-addon-network")
class HassioAddonNetwork extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@property({ type: Boolean }) public disabled = false;
@state() private _showOptional = false;
@state() private _configHasChanged = false;
@state() private _error?: string;
@state() private _config?: Record<string, any>;
public connectedCallback(): void {
super.connectedCallback();
this._setNetworkConfig();
}
protected render() {
if (!this._config) {
return nothing;
}
const hasHiddenOptions = Object.keys(this._config).find(
(entry) => this._config![entry] === null
);
return html`
<ha-card
outlined
.header=${this.supervisor.localize("app.configuration.network.header")}
>
<div class="card-content">
<p>
${this.supervisor.localize(
"app.configuration.network.introduction"
)}
</p>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<ha-form
.disabled=${this.disabled}
.data=${this._config}
@value-changed=${this._configChanged}
.computeLabel=${this._computeLabel}
.computeHelper=${this._computeHelper}
.schema=${this._createSchema(
this._config,
this._showOptional,
this.hass.userData?.showAdvanced || false
)}
></ha-form>
</div>
${hasHiddenOptions
? html`<ha-formfield
class="show-optional"
.label=${this.supervisor.localize(
"app.configuration.network.show_disabled"
)}
>
<ha-switch
@change=${this._toggleOptional}
.checked=${this._showOptional}
>
</ha-switch>
</ha-formfield>`
: nothing}
<div class="card-actions">
<ha-progress-button
variant="danger"
appearance="plain"
.disabled=${this.disabled}
@click=${this._resetTapped}
>
${this.supervisor.localize("common.reset_defaults")}
</ha-progress-button>
<ha-progress-button
@click=${this._saveTapped}
.disabled=${!this._configHasChanged || this.disabled}
>
${this.supervisor.localize("common.save")}
</ha-progress-button>
</div>
</ha-card>
`;
}
protected update(changedProperties: PropertyValues): void {
super.update(changedProperties);
if (changedProperties.has("addon")) {
this._setNetworkConfig();
}
}
private _createSchema = memoizeOne(
(
config: Record<string, number>,
showOptional: boolean,
advanced: boolean
): HaFormSchema[] =>
(showOptional
? Object.keys(config)
: Object.keys(config).filter((entry) => config[entry] !== null)
).map((entry) => ({
name: entry,
selector: {
number: {
mode: "box",
min: 0,
max: 65535,
unit_of_measurement: advanced ? entry : undefined,
},
},
}))
);
private _computeLabel = (_: HaFormSchema): string => "";
private _computeHelper = (item: HaFormSchema): string =>
this.addon.translations[this.hass.language]?.network?.[item.name] ||
this.addon.translations.en?.network?.[item.name] ||
this.addon.network_description?.[item.name] ||
item.name;
private _setNetworkConfig(): void {
this._config = this.addon.network || {};
}
private async _configChanged(ev: CustomEvent): Promise<void> {
this._configHasChanged = true;
this._config = ev.detail.value;
}
private async _resetTapped(ev: CustomEvent): Promise<void> {
if (this.disabled) {
return;
}
const button = ev.currentTarget as any;
const data: HassioAddonSetOptionParams = {
network: null,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
this._configHasChanged = false;
const eventdata = {
success: true,
response: undefined,
path: "option",
};
button.actionSuccess();
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
}
} catch (err: any) {
this._error = this.supervisor.localize("app.failed_to_reset", {
error: extractApiErrorMessage(err),
});
button.actionError();
}
}
private _toggleOptional() {
this._showOptional = !this._showOptional;
}
private async _saveTapped(ev: CustomEvent): Promise<void> {
if (!this._configHasChanged || this.disabled) {
return;
}
const button = ev.currentTarget as any;
this._error = undefined;
const networkconfiguration = {};
Object.entries(this._config!).forEach(([key, value]) => {
networkconfiguration[key] = value ?? null;
});
const data: HassioAddonSetOptionParams = {
network: networkconfiguration,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
this._configHasChanged = false;
const eventdata = {
success: true,
response: undefined,
path: "option",
};
button.actionSuccess();
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
}
} catch (err: any) {
this._error = this.supervisor.localize("app.failed_to_save", {
error: extractApiErrorMessage(err),
});
button.actionError();
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
:host {
display: block;
}
ha-card {
display: block;
}
.card-actions {
display: flex;
justify-content: space-between;
}
.show-optional {
padding: 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-network": HassioAddonNetwork;
}
}
@@ -1,97 +0,0 @@
import "../../../../src/components/ha-card";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-spinner";
import "../../../../src/components/ha-markdown";
import { customElement, property, state } from "lit/decorators";
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { fetchHassioAddonDocumentation } from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import "../../../../src/layouts/hass-loading-screen";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
@customElement("hassio-addon-documentation-tab")
class HassioAddonDocumentationDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon?: HassioAddonDetails;
@state() private _error?: string;
@state() private _content?: string;
public async connectedCallback(): Promise<void> {
super.connectedCallback();
await this._loadData();
}
protected render(): TemplateResult {
if (!this.addon) {
return html`<ha-spinner></ha-spinner>`;
}
return html`
<div class="content">
<ha-card outlined>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="card-content">
${this._content
? html`<ha-markdown
.content=${this._content}
lazy-images
></ha-markdown>`
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
</div>
</ha-card>
</div>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
ha-card {
display: block;
}
.content {
margin: auto;
padding: 8px;
max-width: 1024px;
}
ha-markdown {
padding: 16px;
}
`,
];
}
private async _loadData(): Promise<void> {
this._error = undefined;
try {
this._content = await fetchHassioAddonDocumentation(
this.hass,
this.addon!.slug
);
} catch (err: any) {
this._error = this.supervisor.localize(
"app.documentation.get_documentation",
{ error: extractApiErrorMessage(err) }
);
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-documentation-tab": HassioAddonDocumentationDashboard;
}
}
@@ -1,294 +0,0 @@
import {
mdiCogs,
mdiFileDocument,
mdiInformationVariant,
mdiTextBoxOutline,
} from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { navigate } from "../../../src/common/navigate";
import { extractSearchParam } from "../../../src/common/url/search-params";
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
import {
fetchAddonInfo,
fetchHassioAddonInfo,
fetchHassioAddonsInfo,
} from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import type { StoreAddonDetails } from "../../../src/data/supervisor/store";
import {
addStoreRepository,
fetchSupervisorStore,
} from "../../../src/data/supervisor/store";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-error-screen";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles";
import type { HomeAssistant, Route } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style";
import "./config/hassio-addon-audio";
import "./config/hassio-addon-config";
import "./config/hassio-addon-network";
import "./hassio-addon-router";
import "./info/hassio-addon-info";
@customElement("hassio-addon-dashboard")
class HassioAddonDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public addon?:
| HassioAddonDetails
| StoreAddonDetails;
@property({ type: Boolean }) public narrow = false;
@state()
private _controlEnabled = false;
@state() private _error?: string;
private _backPath = new URLSearchParams(window.parent.location.search).get(
"store"
)
? "/hassio/store"
: "/hassio/dashboard";
private _computeTail = memoizeOne((route: Route) => {
const dividerPos = route.path.indexOf("/", 1);
return dividerPos === -1
? {
prefix: route.prefix + route.path,
path: "",
}
: {
prefix: route.prefix + route.path.substr(0, dividerPos),
path: route.path.substr(dividerPos),
};
});
protected render(): TemplateResult {
if (this._error) {
return html`<hass-error-screen
.error=${this._error}
></hass-error-screen>`;
}
if (!this.addon || !this.supervisor?.addon) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
const addonTabs: PageNavigation[] = [
{
translationKey: "app.panel.info",
path: `/hassio/addon/${this.addon.slug}/info`,
iconPath: mdiInformationVariant,
},
];
if (this.addon.documentation) {
addonTabs.push({
translationKey: "app.panel.documentation",
path: `/hassio/addon/${this.addon.slug}/documentation`,
iconPath: mdiFileDocument,
});
}
if (this.addon.version) {
addonTabs.push(
{
translationKey: "app.panel.configuration",
path: `/hassio/addon/${this.addon.slug}/config`,
iconPath: mdiCogs,
},
{
translationKey: "app.panel.log",
path: `/hassio/addon/${this.addon.slug}/logs`,
iconPath: mdiTextBoxOutline,
}
);
}
const route = this._computeTail(this.route);
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
.route=${route}
.tabs=${addonTabs}
.backPath=${this._backPath}
supervisor
>
<span slot="header">${this.addon.name}</span>
<hassio-addon-router
.route=${route}
.narrow=${this.narrow}
.hass=${this.hass}
.supervisor=${this.supervisor}
.addon=${this.addon}
.controlEnabled=${this._controlEnabled}
@system-managed-take-control=${this._enableControl}
></hassio-addon-router>
</hass-tabs-subpage>
`;
}
private _enableControl() {
this._controlEnabled = true;
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
:host {
color: var(--primary-text-color);
}
.content {
padding: 24px 0 32px;
display: flex;
flex-direction: column;
align-items: center;
}
hassio-addon-info,
hassio-addon-network,
hassio-addon-audio,
hassio-addon-config {
margin-bottom: 24px;
width: 600px;
}
@media only screen and (max-width: 600px) {
hassio-addon-info,
hassio-addon-network,
hassio-addon-audio,
hassio-addon-config {
max-width: 100%;
min-width: 100%;
}
}
`,
];
}
protected async firstUpdated(): Promise<void> {
if (this.route.path === "") {
const requestedAddon = extractSearchParam("addon");
const requestedAddonRepository = extractSearchParam("repository_url");
if (requestedAddonRepository) {
const storeInfo = await fetchSupervisorStore(this.hass);
if (
!storeInfo.repositories.find(
(repo) => repo.source === requestedAddonRepository
)
) {
if (
!(await showConfirmationDialog(this, {
title: this.supervisor.localize("my.add_app_repository_title"),
text: this.supervisor.localize(
"my.add_app_repository_description",
{ app: requestedAddon, repository: requestedAddonRepository }
),
confirmText: this.supervisor.localize("common.add"),
dismissText: this.supervisor.localize("common.cancel"),
}))
) {
this._error = this.supervisor.localize(
"my.error_repository_not_found"
);
return;
}
try {
await addStoreRepository(this.hass, requestedAddonRepository);
} catch (err: any) {
this._error = extractApiErrorMessage(err);
}
}
}
if (requestedAddon) {
const store = await fetchSupervisorStore(this.hass);
const validAddon = store.addons.some(
(addon) => addon.slug === requestedAddon
);
if (!validAddon) {
this._error = this.supervisor.localize("my.error_app_not_found");
} else {
navigate(`/hassio/addon/${requestedAddon}`, { replace: true });
}
}
}
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
}
private async _apiCalled(ev): Promise<void> {
if (!ev.detail.success) {
return;
}
const pathSplit: string[] = ev.detail.path?.split("/");
if (!pathSplit || pathSplit.length === 0) {
return;
}
const path: string = pathSplit[pathSplit.length - 1];
if (["uninstall", "install", "update", "start", "stop"].includes(path)) {
fireEvent(this, "supervisor-collection-refresh", {
collection: "addon",
});
}
if (path === "uninstall") {
if (this.isConnected) {
navigate(this._backPath);
}
} else if (path === "install") {
this.addon = await fetchHassioAddonInfo(this.hass, this.addon!.slug);
} else {
await this._routeDataChanged();
}
}
protected updated(changedProperties) {
if (changedProperties.has("route") && !this.addon) {
this._routeDataChanged();
}
}
private async _routeDataChanged(): Promise<void> {
const addon = this.route.path.split("/")[1];
if (!addon) {
return;
}
try {
if (!this.supervisor.addon) {
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
fireEvent(this, "supervisor-update", { addon: addonsInfo });
}
this.addon = await fetchAddonInfo(this.hass, this.supervisor, addon);
} catch (err: any) {
this._error = `Error fetching addon info: ${extractApiErrorMessage(err)}`;
this.addon = undefined;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-dashboard": HassioAddonDashboard;
}
}
@@ -1,62 +0,0 @@
import { customElement, property } from "lit/decorators";
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
import type { StoreAddonDetails } from "../../../src/data/supervisor/store";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import type { RouterOptions } from "../../../src/layouts/hass-router-page";
import { HassRouterPage } from "../../../src/layouts/hass-router-page";
import type { HomeAssistant } from "../../../src/types";
import "./config/hassio-addon-config-tab";
import "./documentation/hassio-addon-documentation-tab";
// Don't codesplit the others, because it breaks the UI when pushed to a Pi
import "./info/hassio-addon-info-tab";
import "./log/hassio-addon-log-tab";
@customElement("hassio-addon-router")
class HassioAddonRouter extends HassRouterPage {
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon!:
| HassioAddonDetails
| StoreAddonDetails;
@property({ type: Boolean, attribute: "control-enabled" })
public controlEnabled = false;
protected routerOptions: RouterOptions = {
defaultPage: "info",
showLoading: true,
routes: {
info: {
tag: "hassio-addon-info-tab",
},
documentation: {
tag: "hassio-addon-documentation-tab",
},
config: {
tag: "hassio-addon-config-tab",
},
logs: {
tag: "hassio-addon-log-tab",
},
},
};
protected updatePageEl(el) {
el.route = this.routeTail;
el.hass = this.hass;
el.supervisor = this.supervisor;
el.addon = this.addon;
el.narrow = this.narrow;
el.controlEnabled = this.controlEnabled;
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-router": HassioAddonRouter;
}
}
@@ -1,65 +0,0 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../src/components/ha-spinner";
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant, Route } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
import "./hassio-addon-info";
@customElement("hassio-addon-info-tab")
class HassioAddonInfoDashboard extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon?: HassioAddonDetails;
@property({ type: Boolean, attribute: "control-enabled" })
public controlEnabled = false;
protected render(): TemplateResult {
if (!this.addon) {
return html`<ha-spinner></ha-spinner>`;
}
return html`
<div class="content">
<hassio-addon-info
.narrow=${this.narrow}
.route=${this.route}
.hass=${this.hass}
.supervisor=${this.supervisor}
.addon=${this.addon}
.controlEnabled=${this.controlEnabled}
></hassio-addon-info>
</div>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
.content {
margin: auto;
padding: 8px;
max-width: 1024px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-info-tab": HassioAddonInfoDashboard;
}
}
File diff suppressed because it is too large Load Diff
@@ -1,59 +0,0 @@
import type { TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
@customElement("hassio-addon-system-managed")
class HassioAddonSystemManaged extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean, attribute: "hide-button" }) public hideButton =
false;
protected render(): TemplateResult {
return html`
<ha-alert
alert-type="warning"
.title=${this.supervisor.localize("app.system_managed.title")}
.narrow=${this.narrow}
>
${this.supervisor.localize("app.system_managed.description")}
${!this.hideButton
? html`
<ha-button slot="action" @click=${this._takeControl}>
${this.supervisor.localize("app.system_managed.take_control")}
</ha-button>
`
: nothing}
</ha-alert>
`;
}
private _takeControl() {
fireEvent(this, "system-managed-take-control");
}
static styles = css`
ha-alert {
display: block;
margin-bottom: 16px;
}
ha-button {
white-space: nowrap;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-system-managed": HassioAddonSystemManaged;
}
interface HASSDomEvents {
"system-managed-take-control": undefined;
}
}
@@ -1,92 +0,0 @@
import {
css,
type CSSResultGroup,
html,
LitElement,
type TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-spinner";
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
import "../../../../src/panels/config/logs/error-log-card";
import "../../../../src/components/search-input";
import { extractSearchParam } from "../../../../src/common/url/search-params";
@customElement("hassio-addon-log-tab")
class HassioAddonLogDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon?: HassioAddonDetails;
@state() private _filter = extractSearchParam("filter") || "";
protected render(): TemplateResult {
if (!this.addon) {
return html` <ha-spinner></ha-spinner> `;
}
return html`
<div class="search">
<search-input
@value-changed=${this._filterChanged}
.hass=${this.hass}
.filter=${this._filter}
.label=${this.supervisor.localize("ui.panel.config.logs.search")}
></search-input>
</div>
<div class="content">
<error-log-card
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.header=${this.addon.name}
.provider=${this.addon.slug}
.filter=${this._filter}
>
</error-log-card>
</div>
`;
}
private async _filterChanged(ev) {
this._filter = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
.content {
margin: auto;
padding: 8px;
}
.search {
position: sticky;
top: 0;
z-index: 2;
}
search-input {
display: block;
--mdc-text-field-fill-color: var(--sidebar-background-color);
--mdc-text-field-idle-line-color: var(--divider-color);
}
@media all and (max-width: 870px) {
:host {
--error-log-card-height: calc(100vh - 304px);
}
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-log-tab": HassioAddonLogDashboard;
}
}
-425
View File
@@ -1,425 +0,0 @@
import type { ActionDetail } from "@material/mwc-list";
import { mdiBackupRestore, mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { relativeTime } from "../../../src/common/datetime/relative_time";
import type { HASSDomEvent } from "../../../src/common/dom/fire_event";
import type {
DataTableColumnContainer,
RowClickedEvent,
SelectionChangedEvent,
} from "../../../src/components/data-table/ha-data-table";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-fab";
import "../../../src/components/ha-button";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-list-item";
import "../../../src/components/ha-svg-icon";
import type { HassioBackup } from "../../../src/data/hassio/backup";
import {
fetchHassioBackups,
friendlyFolderName,
reloadHassioBackups,
removeBackup,
} from "../../../src/data/hassio/backup";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-tabs-subpage-data-table";
import type { HaTabsSubpageDataTable } from "../../../src/layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../src/resources/styles";
import type { HomeAssistant, Route } from "../../../src/types";
import { showBackupUploadDialog } from "../dialogs/backup/show-dialog-backup-upload";
import { showHassioBackupLocationDialog } from "../dialogs/backup/show-dialog-hassio-backu-location";
import { showHassioBackupDialog } from "../dialogs/backup/show-dialog-hassio-backup";
import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hassio-create-backup";
import { supervisorTabs } from "../hassio-tabs";
import { hassioStyle } from "../resources/hassio-style";
type BackupItem = HassioBackup & {
secondary: string;
};
@customElement("hassio-backups")
export class HassioBackups extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@state() private _selectedBackups: string[] = [];
@state() private _backups?: HassioBackup[] = [];
@state() private _isLoading = false;
@query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable;
private _firstUpdatedCalled = false;
public connectedCallback(): void {
super.connectedCallback();
if (this.hass && this._firstUpdatedCalled) {
this._fetchBackups();
}
}
private _computeBackupContent = (backup: HassioBackup): string => {
if (backup.type === "full") {
return this.supervisor.localize("backup.full_backup");
}
const content: string[] = [];
if (backup.content.homeassistant) {
content.push("Home Assistant");
}
if (backup.content.folders.length !== 0) {
for (const folder of backup.content.folders) {
content.push(friendlyFolderName[folder] || folder);
}
}
if (backup.content.addons.length !== 0) {
for (const addon of backup.content.addons) {
content.push(
this.supervisor.addon.addons.find((entry) => entry.slug === addon)
?.name || addon
);
}
}
return content.join(", ");
};
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
if (this.hass && this.isConnected) {
this._fetchBackups();
}
this._firstUpdatedCalled = true;
}
private _columns = memoizeOne(
(narrow: boolean): DataTableColumnContainer<BackupItem> => ({
name: {
title: this.supervisor.localize("backup.name"),
main: true,
sortable: true,
filterable: true,
flex: 2,
template: (backup) =>
html`${backup.name || backup.slug}
<div class="secondary">${backup.secondary}</div>`,
},
size: {
title: this.supervisor.localize("backup.size"),
hidden: narrow,
filterable: true,
sortable: true,
template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB",
},
location: {
title: this.supervisor.localize("backup.location"),
hidden: narrow,
filterable: true,
sortable: true,
template: (backup) =>
backup.location || this.supervisor.localize("backup.data_disk"),
},
date: {
title: this.supervisor.localize("backup.created"),
direction: "desc",
hidden: narrow,
filterable: true,
sortable: true,
template: (backup) =>
relativeTime(new Date(backup.date), this.hass.locale),
},
secondary: {
title: "",
hidden: true,
filterable: true,
},
})
);
private _backupData = memoizeOne((backups: HassioBackup[]): BackupItem[] =>
backups.map((backup) => ({
...backup,
secondary: this._computeBackupContent(backup),
}))
);
protected render() {
if (!this.supervisor) {
return nothing;
}
if (this._isLoading) {
return html`<hass-loading-screen
.message=${this.supervisor.localize("backup.loading_backups")}
></hass-loading-screen>`;
}
return html`
<hass-tabs-subpage-data-table
.tabs=${atLeastVersion(this.hass.config.version, 2022, 5)
? [
{
translationKey: "panel.backups",
path: `/hassio/backups`,
iconPath: mdiBackupRestore,
},
]
: supervisorTabs(this.hass)}
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.searchLabel=${this.supervisor.localize("backup.search")}
.noDataText=${this.supervisor.localize("backup.no_backups")}
.narrow=${this.narrow}
.route=${this.route}
.columns=${this._columns(this.narrow)}
.data=${this._backupData(this._backups || [])}
id="slug"
@row-click=${this._handleRowClicked}
@selection-changed=${this._handleSelectionChanged}
clickable
selectable
has-fab
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
back-path=${atLeastVersion(this.hass.config.version, 2022, 5)
? "/config/system"
: "/config"}
supervisor
>
<ha-button-menu slot="toolbar-icon" @action=${this._handleAction}>
<ha-icon-button
.label=${this.supervisor?.localize("common.menu")}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<ha-list-item>
${this.supervisor.localize("common.reload")}
</ha-list-item>
<ha-list-item>
${this.supervisor.localize("dialog.backup_location.title")}
</ha-list-item>
${atLeastVersion(this.hass.config.version, 0, 116)
? html`<ha-list-item>
${this.supervisor.localize("backup.upload_backup")}
</ha-list-item>`
: ""}
</ha-button-menu>
${this._selectedBackups.length
? html`<div
class=${classMap({
"header-toolbar": this.narrow,
"table-header": !this.narrow,
})}
slot="header"
>
<p class="selected-txt">
${this.supervisor.localize("backup.selected", {
number: this._selectedBackups.length,
})}
</p>
<div class="header-btns">
${!this.narrow
? html`
<ha-button
appearance="plain"
variant="danger"
@click=${this._deleteSelected}
>
${this.supervisor.localize("backup.delete_selected")}
</ha-button>
`
: html`
<ha-icon-button
.label=${this.supervisor.localize(
"backup.delete_selected"
)}
.path=${mdiDelete}
class="warning"
@click=${this._deleteSelected}
></ha-icon-button>
`}
</div>
</div> `
: ""}
<ha-fab
slot="fab"
@click=${this._createBackup}
.label=${this.supervisor.localize("backup.create_backup")}
extended
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</hass-tabs-subpage-data-table>
`;
}
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._fetchBackups();
break;
case 1:
showHassioBackupLocationDialog(this, { supervisor: this.supervisor });
break;
case 2:
this._showUploadBackupDialog();
break;
}
}
private _handleSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selectedBackups = ev.detail.value;
}
private _showUploadBackupDialog() {
showBackupUploadDialog(this, {
showBackup: (slug: string) =>
showHassioBackupDialog(this, {
slug,
supervisor: this.supervisor,
onDelete: () => this._fetchBackups(),
}),
reloadBackup: () => this._fetchBackups(),
});
}
private async _fetchBackups() {
this._isLoading = true;
await reloadHassioBackups(this.hass);
this._backups = await fetchHassioBackups(this.hass);
this._isLoading = false;
}
private async _deleteSelected() {
const confirm = await showConfirmationDialog(this, {
title: this.supervisor.localize("backup.delete_backup_title"),
text: this.supervisor.localize("backup.delete_backup_text", {
number: this._selectedBackups.length,
}),
confirmText: this.supervisor.localize("backup.delete_backup_confirm"),
destructive: true,
});
if (!confirm) {
return;
}
try {
await Promise.all(
this._selectedBackups.map((slug) => removeBackup(this.hass, slug))
);
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize("backup.failed_to_delete"),
text: extractApiErrorMessage(err),
});
return;
}
await this._fetchBackups();
this._dataTable.clearSelection();
}
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const slug = ev.detail.id;
showHassioBackupDialog(this, {
slug,
supervisor: this.supervisor,
onDelete: () => this._fetchBackups(),
});
}
private _createBackup() {
if (this.supervisor!.info.state !== "running") {
showAlertDialog(this, {
title: this.supervisor!.localize("backup.could_not_create"),
text: this.supervisor!.localize("backup.create_blocked_not_running", {
state: this.supervisor!.info.state,
}),
});
return;
}
showHassioCreateBackupDialog(this, {
supervisor: this.supervisor!,
onCreate: () => this._fetchBackups(),
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
:host {
color: var(--primary-text-color);
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 58px;
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
}
.header-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--secondary-text-color);
position: relative;
top: -4px;
}
.selected-txt {
font-weight: var(--ha-font-weight-bold);
padding-left: 16px;
padding-inline-start: 16px;
padding-inline-end: initial;
color: var(--primary-text-color);
}
.table-header .selected-txt {
margin-top: 20px;
}
.header-toolbar .selected-txt {
font-size: var(--ha-font-size-l);
}
.header-toolbar .header-btns {
margin-right: -12px;
margin-inline-end: -12px;
margin-inline-start: initial;
}
.header-btns > ha-button,
.header-btns > ha-icon-button {
margin: 8px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-backups": HassioBackups;
}
}
@@ -1,151 +0,0 @@
import { mdiHelpCircle } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../src/components/ha-svg-icon";
import type { HomeAssistant } from "../../../src/types";
@customElement("hassio-card-content")
class HassioCardContent extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@property() public title!: string;
@property() public description?: string;
@property({ type: Boolean }) public available = true;
@property({ attribute: false }) public showTopbar = false;
@property({ attribute: false }) public topbarClass?: string;
@property({ attribute: false }) public iconTitle?: string;
@property({ attribute: false }) public iconClass?: string;
@property() public icon = mdiHelpCircle;
@property({ attribute: false }) public iconImage?: string;
protected render(): TemplateResult {
return html`
${this.showTopbar
? html` <div class="topbar ${this.topbarClass}"></div> `
: ""}
${this.iconImage
? html`
<div class="icon_image ${this.iconClass}">
<img
src=${this.iconImage}
.title=${this.iconTitle}
alt=${this.iconTitle ?? ""}
/>
<div></div>
</div>
`
: html`
<ha-svg-icon
class=${this.iconClass!}
.path=${this.icon}
.title=${this.iconTitle}
></ha-svg-icon>
`}
<div>
<div class="title">${this.title}</div>
<div class="addition">
${this.description}
${
/* treat as available when undefined */
this.available === false ? " (Not available)" : ""
}
</div>
</div>
`;
}
static styles = css`
:host {
direction: ltr;
}
ha-svg-icon {
margin-right: 24px;
margin-left: 8px;
margin-top: 12px;
float: left;
color: var(--secondary-text-color);
}
ha-svg-icon.update {
color: var(--warning-color);
}
ha-svg-icon.running,
ha-svg-icon.installed {
color: var(--success-color);
}
ha-svg-icon.hassupdate,
ha-svg-icon.backup {
color: var(--state-icon-color);
}
ha-svg-icon.not_available {
color: var(--error-color);
}
.title {
color: var(--primary-text-color);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.addition {
color: var(--secondary-text-color);
overflow: hidden;
position: relative;
height: 2.4em;
line-height: var(--ha-line-height-condensed);
}
.icon_image img {
max-height: 40px;
max-width: 40px;
margin-top: 4px;
margin-right: 16px;
float: left;
}
.icon_image.stopped,
.icon_image.not_available {
filter: grayscale(1);
}
.dot {
position: absolute;
background-color: var(--warning-color);
width: 12px;
height: 12px;
top: 8px;
right: 8px;
border-radius: var(--ha-border-radius-circle);
}
.topbar {
position: absolute;
width: 100%;
height: 2px;
top: 0;
left: 0;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.topbar.installed {
background-color: var(--primary-color);
}
.topbar.update {
background-color: var(--accent-color);
}
.topbar.unavailable {
background-color: var(--error-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hassio-card-content": HassioCardContent;
}
}
@@ -1,15 +0,0 @@
import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import type { StoreAddon } from "../../../src/data/supervisor/store";
export function filterAndSort(addons: StoreAddon[], filter: string) {
const options: IFuseOptions<StoreAddon> = {
keys: ["name", "description", "slug"],
isCaseSensitive: false,
minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2,
ignoreDiacritics: true,
};
const fuse = new Fuse(addons, options);
return fuse.search(filter).map((result) => result.item);
}
@@ -1,89 +0,0 @@
import { mdiFolderUpload } from "@mdi/js";
import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-file-upload";
import type { HassioBackup } from "../../../src/data/hassio/backup";
import { uploadBackup } from "../../../src/data/hassio/backup";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../src/types";
import type { LocalizeFunc } from "../../../src/common/translations/localize";
declare global {
interface HASSDomEvents {
"hassio-backup-uploaded": { backup: HassioBackup };
"backup-cleared": undefined;
}
}
@customElement("hassio-upload-backup")
export class HassioUploadBackup extends LitElement {
public hass?: HomeAssistant;
@property({ attribute: false }) public localize?: LocalizeFunc;
@state() public value: string | null = null;
@state() private _uploading = false;
public render(): TemplateResult {
return html`
<ha-file-upload
.hass=${this.hass}
.uploading=${this._uploading}
.icon=${mdiFolderUpload}
accept="application/x-tar"
.label=${this.localize?.(
"ui.panel.page-onboarding.restore.upload_backup"
) || "Upload backup"}
.supports=${this.localize?.(
"ui.panel.page-onboarding.restore.upload_supports"
) || "Supports .TAR files"}
.secondary=${this.localize?.(
"ui.panel.page-onboarding.restore.upload_drop"
) || "Or drop your file here"}
@file-picked=${this._uploadFile}
@files-cleared=${this._clear}
></ha-file-upload>
`;
}
private _clear() {
this.value = null;
fireEvent(this, "backup-cleared");
}
private async _uploadFile(ev) {
const file = ev.detail.files[0];
if (!["application/x-tar"].includes(file.type)) {
showAlertDialog(this, {
title: "Unsupported file format",
text: "Please choose a Home Assistant backup file (.tar)",
confirmText: "ok",
});
return;
}
this._uploading = true;
try {
const backup = await uploadBackup(this.hass, file);
fireEvent(this, "hassio-backup-uploaded", { backup: backup.data });
} catch (err: any) {
showAlertDialog(this, {
title: "Upload failed",
text: extractApiErrorMessage(err),
confirmText: "ok",
});
} finally {
this._uploading = false;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-upload-backup": HassioUploadBackup;
}
}
@@ -1,460 +0,0 @@
import { mdiFolder, mdiPuzzle } from "@mdi/js";
import type { TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import { formatDate } from "../../../src/common/datetime/format_date";
import { formatDateTime } from "../../../src/common/datetime/format_date_time";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-textfield";
import "../../../src/components/ha-password-field";
import "../../../src/components/ha-radio";
import type { HaRadio } from "../../../src/components/ha-radio";
import type {
HassioBackupDetail,
HassioFullBackupCreateParams,
HassioPartialBackupCreateParams,
} from "../../../src/data/hassio/backup";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg";
import type { HomeAssistant } from "../../../src/types";
import "./supervisor-formfield-label";
import type { HaTextField } from "../../../src/components/ha-textfield";
interface CheckboxItem {
slug: string;
checked: boolean;
name: string;
}
interface AddonCheckboxItem extends CheckboxItem {
version: string;
}
const _computeFolders = (folders): CheckboxItem[] => {
const list: CheckboxItem[] = [];
if (folders.includes("ssl")) {
list.push({ slug: "ssl", name: "SSL", checked: false });
}
if (folders.includes("share")) {
list.push({ slug: "share", name: "Share", checked: false });
}
if (folders.includes("media")) {
list.push({ slug: "media", name: "Media", checked: false });
}
if (folders.includes("addons/local")) {
list.push({ slug: "addons/local", name: "Local add-ons", checked: false });
}
return list.sort((a, b) => (a.name > b.name ? 1 : -1));
};
const _computeAddons = (addons): AddonCheckboxItem[] =>
addons
.map((addon) => ({
slug: addon.slug,
name: addon.name,
version: addon.version,
checked: false,
}))
.sort((a, b) => (a.name > b.name ? 1 : -1));
@customElement("supervisor-backup-content")
export class SupervisorBackupContent extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public supervisor?: Supervisor;
@property({ attribute: false }) public backup?: HassioBackupDetail;
@property({ attribute: false })
public backupType: HassioBackupDetail["type"] = "full";
@property({ attribute: false }) public folders?: CheckboxItem[];
@property({ attribute: false }) public addons?: AddonCheckboxItem[];
@property({ attribute: false }) public homeAssistant = false;
@property({ attribute: false }) public backupHasPassword = false;
@property({ type: Boolean }) public onboarding = false;
@property({ attribute: false }) public backupName = "";
@property({ attribute: false }) public backupPassword = "";
@property({ attribute: false }) public confirmBackupPassword = "";
@query("ha-textfield, ha-radio, ha-checkbox", true) private _focusTarget;
public willUpdate(changedProps) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this.folders = _computeFolders(
this.backup
? this.backup.folders
: ["ssl", "share", "media", "addons/local"]
);
this.addons = _computeAddons(
this.backup ? this.backup.addons : this.supervisor?.addon.addons
);
this.backupType = this.backup?.type || "full";
this.backupName = this.backup?.name || "";
this.backupHasPassword = this.backup?.protected || false;
}
}
public override focus() {
this._focusTarget?.focus();
}
protected render() {
if (!this.onboarding && !this.supervisor) {
return nothing;
}
const foldersSection =
this.backupType === "partial" ? this._getSection("folders") : undefined;
const addonsSection =
this.backupType === "partial" ? this._getSection("addons") : undefined;
return html`
${this.backup
? html`<div class="details">
${this.backup.type === "full"
? this.supervisor?.localize("backup.full_backup")
: this.supervisor?.localize("backup.partial_backup")}
(${Math.ceil(this.backup.size * 10) / 10 + " MB"})<br />
${this.hass
? formatDateTime(
new Date(this.backup.date),
this.hass.locale,
this.hass.config
)
: this.backup.date}
</div>`
: html`<ha-textfield
name="backupName"
.label=${this.supervisor?.localize("backup.name")}
.value=${this.backupName}
@change=${this._handleTextValueChanged}
>
</ha-textfield>`}
${!this.backup || this.backup.type === "full"
? html`<div class="sub-header">
${!this.backup
? this.supervisor?.localize("backup.type")
: this.supervisor?.localize("backup.select_type")}
</div>
<div class="backup-types">
<ha-formfield
.label=${this.supervisor?.localize("backup.full_backup")}
>
<ha-radio
@change=${this._handleRadioValueChanged}
value="full"
name="backupType"
.checked=${this.backupType === "full"}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.supervisor?.localize("backup.partial_backup")}
>
<ha-radio
@change=${this._handleRadioValueChanged}
value="partial"
name="backupType"
.checked=${this.backupType === "partial"}
>
</ha-radio>
</ha-formfield>
</div>`
: ""}
${this.backupType === "partial"
? html`<div class="partial-picker">
${!this.backup || this.backup.homeassistant
? html`<ha-formfield
.label=${html`<supervisor-formfield-label
label="Home Assistant"
.iconPath=${mdiHomeAssistant}
.version=${this.backup
? this.backup.homeassistant
: this.hass?.config.version}
>
</supervisor-formfield-label>`}
>
<ha-checkbox
.checked=${this.onboarding || this.homeAssistant}
.disabled=${this.onboarding}
@change=${this._toggleHomeAssistant}
>
</ha-checkbox>
</ha-formfield>`
: ""}
${foldersSection?.templates.length
? html`
<ha-formfield
.label=${html`<supervisor-formfield-label
.label=${this.supervisor?.localize("backup.folders")}
.iconPath=${mdiFolder}
>
</supervisor-formfield-label>`}
>
<ha-checkbox
@change=${this._toggleSection}
.checked=${foldersSection.checked}
.indeterminate=${foldersSection.indeterminate}
.section=${"folders"}
>
</ha-checkbox>
</ha-formfield>
<div class="section-content">${foldersSection.templates}</div>
`
: ""}
${addonsSection?.templates.length
? html`
<ha-formfield
.label=${html`<supervisor-formfield-label
.label=${this.supervisor?.localize("backup.apps")}
.iconPath=${mdiPuzzle}
>
</supervisor-formfield-label>`}
>
<ha-checkbox
@change=${this._toggleSection}
.checked=${addonsSection.checked}
.indeterminate=${addonsSection.indeterminate}
.section=${"addons"}
>
</ha-checkbox>
</ha-formfield>
<div class="section-content">${addonsSection.templates}</div>
`
: ""}
</div> `
: ""}
${this.backupType === "partial" &&
(!this.backup || this.backupHasPassword)
? html`<hr />`
: ""}
${!this.backup
? html`<ha-formfield
class="password"
.label=${this.supervisor?.localize("backup.password_protection")}
>
<ha-checkbox
.checked=${this.backupHasPassword}
@change=${this._toggleHasPassword}
>
</ha-checkbox>
</ha-formfield>`
: ""}
${this.backupHasPassword
? html`
<ha-password-field
.label=${this.supervisor?.localize("backup.password")}
name="backupPassword"
.value=${this.backupPassword}
@change=${this._handleTextValueChanged}
>
</ha-password-field>
${!this.backup
? html`<ha-password-field
.label=${this.supervisor?.localize("backup.confirm_password")}
name="confirmBackupPassword"
.value=${this.confirmBackupPassword}
@change=${this._handleTextValueChanged}
>
</ha-password-field>`
: ""}
`
: ""}
`;
}
private _toggleHomeAssistant() {
this.homeAssistant = !this.homeAssistant;
}
static styles = css`
.partial-picker ha-formfield {
display: block;
}
.partial-picker ha-checkbox {
--mdc-checkbox-touch-target-size: 32px;
}
.partial-picker {
display: block;
margin: 0px -6px;
}
supervisor-formfield-label {
display: inline-flex;
align-items: center;
}
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: 16px 0;
}
.details {
color: var(--secondary-text-color);
}
.section-content {
display: flex;
flex-direction: column;
margin-left: 30px;
margin-inline-start: 30px;
margin-inline-end: initial;
}
ha-formfield.password {
display: block;
margin: 0 -14px -16px;
}
.backup-types {
display: flex;
margin-left: -13px;
margin-inline-start: -13px;
margin-inline-end: initial;
}
.sub-header {
margin-top: 8px;
}
`;
public backupDetails():
| HassioPartialBackupCreateParams
| HassioFullBackupCreateParams {
const data: any = {};
if (!this.backup && this.hass) {
data.name =
this.backupName ||
formatDate(new Date(), this.hass.locale, this.hass.config);
}
if (this.backupHasPassword) {
data.password = this.backupPassword;
if (!this.backup) {
data.confirm_password = this.confirmBackupPassword;
}
}
if (this.backupType === "full") {
return data;
}
const addons = this.addons
?.filter((addon) => addon.checked)
.map((addon) => addon.slug);
const folders = this.folders
?.filter((folder) => folder.checked)
.map((folder) => folder.slug);
if (addons?.length) {
data.addons = addons;
}
if (folders?.length) {
data.folders = folders;
}
// onboarding needs at least homeassistant to restore
data.homeassistant = this.onboarding || this.homeAssistant;
return data;
}
private _getSection(section: string) {
const templates: TemplateResult[] = [];
const addons =
section === "addons"
? new Map(
this.supervisor?.addon.addons.map((item) => [item.slug, item])
)
: undefined;
let checkedItems = 0;
this[section].forEach((item) => {
templates.push(
html`<ha-formfield
.label=${html`<supervisor-formfield-label
.label=${item.name}
.iconPath=${section === "addons" ? mdiPuzzle : mdiFolder}
.imageUrl=${section === "addons" &&
!this.onboarding &&
this.hass &&
atLeastVersion(this.hass.config.version, 0, 105) &&
addons?.get(item.slug)?.icon
? `/api/hassio/addons/${item.slug}/icon`
: undefined}
.version=${item.version}
>
</supervisor-formfield-label>`}
>
<ha-checkbox
.item=${item}
.checked=${item.checked}
.section=${section}
@change=${this._updateSectionEntry}
>
</ha-checkbox>
</ha-formfield>`
);
if (item.checked) {
checkedItems++;
}
});
const checked = checkedItems === this[section].length;
return {
templates,
checked,
indeterminate: !checked && checkedItems !== 0,
};
}
private _handleRadioValueChanged(ev: CustomEvent) {
const input = ev.currentTarget as HaRadio;
this[input.name] = input.value;
}
private _handleTextValueChanged(ev: InputEvent) {
const input = ev.currentTarget as HaTextField;
this[input.name!] = input.value;
}
private _toggleHasPassword(): void {
this.backupHasPassword = !this.backupHasPassword;
}
private _toggleSection(ev): void {
const section = ev.currentTarget.section;
this[section] = (section === "addons" ? this.addons : this.folders)!.map(
(item) => ({
...item,
checked: ev.currentTarget.checked,
})
);
}
private _updateSectionEntry(ev): void {
const item = ev.currentTarget.item;
const section = ev.currentTarget.section;
this[section] = this[section].map((entry) =>
entry.slug === item.slug
? {
...entry,
checked: ev.currentTarget.checked,
}
: entry
);
}
}
declare global {
interface HTMLElementTagNameMap {
"supervisor-backup-content": SupervisorBackupContent;
}
}
@@ -1,60 +0,0 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../src/components/ha-svg-icon";
@customElement("supervisor-formfield-label")
class SupervisorFormfieldLabel extends LitElement {
@property({ type: String }) public label!: string;
@property({ attribute: false }) public imageUrl?: string;
@property({ attribute: false }) public iconPath?: string;
@property({ type: String }) public version?: string;
protected render(): TemplateResult {
return html`
${this.imageUrl
? html`<img loading="lazy" alt="" src=${this.imageUrl} class="icon" />`
: this.iconPath
? html`<ha-svg-icon
.path=${this.iconPath}
class="icon"
></ha-svg-icon>`
: ""}
<span class="label">${this.label}</span>
${this.version
? html`<span class="version">(${this.version})</span>`
: ""}
`;
}
static styles = css`
:host {
display: flex;
align-items: center;
}
.label {
margin-right: 4px;
margin-inline-end: 4px;
margin-inline-start: initial;
}
.version {
color: var(--secondary-text-color);
}
.icon {
max-height: 22px;
max-width: 22px;
margin-right: 8px;
margin-inline-end: 8px;
margin-inline-start: initial;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"supervisor-formfield-label": SupervisorFormfieldLabel;
}
}
@@ -1,75 +0,0 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "../../../src/components/ha-bar";
import "../../../src/components/ha-settings-row";
import { roundWithOneDecimal } from "../../../src/util/calculate";
@customElement("supervisor-metric")
class SupervisorMetric extends LitElement {
@property({ type: Number }) public value!: number;
@property({ type: String }) public description!: string;
@property({ type: String }) public tooltip?: string;
protected render(): TemplateResult {
const roundedValue = roundWithOneDecimal(this.value);
return html`<ha-settings-row>
<span slot="heading"> ${this.description} </span>
<div slot="description" .title=${this.tooltip ?? ""}>
<span class="value"> ${roundedValue} % </span>
<ha-bar
class=${classMap({
"target-warning": roundedValue > 50,
"target-critical": roundedValue > 85,
})}
.value=${this.value}
></ha-bar>
</div>
</ha-settings-row>`;
}
static styles = css`
ha-settings-row {
padding: 0;
height: 54px;
width: 100%;
}
ha-settings-row > div[slot="description"] {
white-space: normal;
color: var(--secondary-text-color);
display: flex;
justify-content: space-between;
}
ha-bar {
--ha-bar-primary-color: var(--hassio-bar-ok-color, var(--success-color));
}
.target-warning {
--ha-bar-primary-color: var(
--hassio-bar-warning-color,
var(--warning-color)
);
}
.target-critical {
--ha-bar-primary-color: var(
--hassio-bar-critical-color,
var(--error-color)
);
}
.value {
width: 48px;
padding-right: 4px;
padding-inline-start: initial;
padding-inline-end: 4px;
flex-shrink: 0;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"supervisor-metric": SupervisorMetric;
}
}
-162
View File
@@ -1,162 +0,0 @@
import { mdiArrowUpBoldCircle, mdiPuzzle } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { navigate } from "../../../src/common/navigate";
import { caseInsensitiveStringCompare } from "../../../src/common/string/compare";
import "../../../src/components/ha-card";
import "../../../src/components/search-input";
import type { HassioAddonInfo } from "../../../src/data/hassio/addon";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../src/resources/styles";
import type { HomeAssistant } from "../../../src/types";
import "../components/hassio-card-content";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-addons")
class HassioAddons extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow = false;
@state() private _filter?: string;
protected render(): TemplateResult {
return html`
<div class="search">
<search-input
.hass=${this.hass}
suffix
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
.label=${this.supervisor.localize("dashboard.search_apps")}
>
</search-input>
</div>
<div class="content">
${!atLeastVersion(this.hass.config.version, 2021, 12)
? html`<h1>${this.supervisor.localize("dashboard.apps")}</h1>`
: ""}
<div class="card-group">
${!this.supervisor.addon.addons.length
? html`
<ha-card outlined>
<div class="card-content">
<button class="link" @click=${this._openStore}>
${this.supervisor.localize("dashboard.no_apps")}
</button>
</div>
</ha-card>
`
: this._getAddons(this.supervisor.addon.addons, this._filter).map(
(addon) => html`
<ha-card outlined .addon=${addon} @click=${this._addonTapped}>
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
.title=${addon.name}
.description=${addon.description}
available
.showTopbar=${addon.update_available}
topbarClass="update"
.icon=${addon.update_available!
? mdiArrowUpBoldCircle
: mdiPuzzle}
.iconTitle=${addon.state !== "started"
? this.supervisor.localize("dashboard.app_stopped")
: addon.update_available!
? this.supervisor.localize(
"dashboard.app_new_version"
)
: this.supervisor.localize("dashboard.app_running")}
.iconClass=${addon.update_available
? addon.state === "started"
? "update"
: "update stopped"
: addon.state === "started"
? "running"
: "stopped"}
.iconImage=${atLeastVersion(
this.hass.config.version,
0,
105
) && addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined}
></hassio-card-content>
</div>
</ha-card>
`
)}
</div>
</div>
`;
}
private _getAddons = memoizeOne(
(addons: HassioAddonInfo[], filter?: string) => {
if (filter) {
addons = addons.filter((addon) => {
const lowerCaseFilter = filter.toLowerCase();
return (
addon.name.toLowerCase().includes(lowerCaseFilter) ||
addon.description.toLowerCase().includes(lowerCaseFilter) ||
addon.slug.toLowerCase().includes(lowerCaseFilter)
);
});
}
return addons.sort((a, b) =>
caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language)
);
}
);
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
ha-card {
cursor: pointer;
overflow: hidden;
direction: ltr;
}
.search {
position: sticky;
top: 0;
z-index: 2;
}
search-input {
display: block;
--mdc-text-field-fill-color: var(--sidebar-background-color);
--mdc-text-field-idle-line-color: var(--divider-color);
}
.content {
margin-bottom: 72px;
}
`,
];
}
private _addonTapped(ev: any): void {
navigate(`/hassio/addon/${ev.currentTarget.addon.slug}/info`);
}
private _openStore(): void {
navigate("/hassio/store");
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addons": HassioAddons;
}
}
-150
View File
@@ -1,150 +0,0 @@
import { mdiRefresh, mdiStorePlus } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-fab";
import { reloadHassioAddons } from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-subpage";
import "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles";
import type { HomeAssistant, Route } from "../../../src/types";
import { supervisorTabs } from "../hassio-tabs";
import "./hassio-addons";
@customElement("hassio-dashboard")
class HassioDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
firstUpdated() {
if (!atLeastVersion(this.hass.config.version, 2022, 5)) {
import("./hassio-update");
}
}
protected render(): TemplateResult {
if (atLeastVersion(this.hass.config.version, 2022, 5)) {
return html`<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
back-path="/config"
.header=${this.supervisor.localize("panel.apps")}
>
<ha-icon-button
slot="toolbar-icon"
@click=${this._handleCheckUpdates}
.path=${mdiRefresh}
.label=${this.supervisor.localize("store.check_updates")}
></ha-icon-button>
<hassio-addons
.hass=${this.hass}
.supervisor=${this.supervisor}
.narrow=${this.narrow}
></hassio-addons>
<a href="/hassio/store">
<ha-fab
.label=${this.supervisor.localize("panel.store")}
extended
class="non-tabs"
>
<ha-svg-icon
slot="icon"
.path=${mdiStorePlus}
></ha-svg-icon></ha-fab
></a>
</hass-subpage>`;
}
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${supervisorTabs(this.hass)}
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
back-path="/config"
supervisor
has-fab
>
<span slot="header">
${this.supervisor.localize(
atLeastVersion(this.hass.config.version, 2021, 12)
? "panel.apps"
: "panel.dashboard"
)}
</span>
<div class="content">
${!atLeastVersion(this.hass.config.version, 2021, 12)
? html`
<hassio-update
.hass=${this.hass}
.supervisor=${this.supervisor}
></hassio-update>
`
: ""}
<hassio-addons
.hass=${this.hass}
.supervisor=${this.supervisor}
></hassio-addons>
</div>
<a href="/hassio/store" slot="fab">
<ha-fab .label=${this.supervisor.localize("panel.store")} extended>
<ha-svg-icon
slot="icon"
.path=${mdiStorePlus}
></ha-svg-icon> </ha-fab
></a>
</hass-tabs-subpage>
`;
}
private async _handleCheckUpdates() {
try {
await reloadHassioAddons(this.hass);
} catch (err) {
showAlertDialog(this, {
text: extractApiErrorMessage(err),
});
} finally {
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.content {
margin: 0 auto;
}
ha-fab.non-tabs {
position: fixed;
right: calc(16px + var(--safe-area-inset-right));
bottom: calc(16px + var(--safe-area-inset-bottom));
inset-inline-end: calc(16px + var(--safe-area-inset-right));
inset-inline-start: initial;
z-index: 1;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-dashboard": HassioDashboard;
}
}
-158
View File
@@ -1,158 +0,0 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import "../../../src/components/ha-card";
import "../../../src/components/ha-button";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon";
import type { HassioHassOSInfo } from "../../../src/data/hassio/host";
import type {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg";
import { haStyle } from "../../../src/resources/styles";
import type { HomeAssistant } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style";
const computeVersion = (key: string, version: string): string =>
key === "os" ? version : `${key}-${version}`;
@customElement("hassio-update")
export class HassioUpdate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
private _pendingUpdates = memoizeOne(
(supervisor: Supervisor): number =>
Object.keys(supervisor).filter(
(value) => supervisor[value].update_available
).length
);
protected render() {
if (!this.supervisor) {
return nothing;
}
const updatesAvailable = this._pendingUpdates(this.supervisor);
if (!updatesAvailable) {
return nothing;
}
return html`
<div class="content">
<h1>
${this.supervisor.localize("common.update_available", {
count: updatesAvailable,
})}
🎉
</h1>
<div class="card-group">
${this._renderUpdateCard(
"Home Assistant Core",
"core",
this.supervisor.core
)}
${this._renderUpdateCard(
"Supervisor",
"supervisor",
this.supervisor.supervisor
)}
${this.supervisor.host.features.includes("haos")
? this._renderUpdateCard(
"Operating System",
"os",
this.supervisor.os
)
: ""}
</div>
</div>
`;
}
private _renderUpdateCard(
name: string,
key: string,
object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo
) {
if (!object.update_available) {
return nothing;
}
return html`
<ha-card outlined>
<div class="card-content">
<div class="icon">
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
</div>
<div class="update-heading">${name}</div>
<ha-settings-row two-line>
<span slot="heading">
${this.supervisor.localize("common.version")}
</span>
<span slot="description">
${computeVersion(key, object.version!)}
</span>
</ha-settings-row>
<ha-settings-row two-line>
<span slot="heading">
${this.supervisor.localize("common.newest_version")}
</span>
<span slot="description">
${computeVersion(key, object.version_latest!)}
</span>
</ha-settings-row>
</div>
<div class="card-actions">
<ha-button appearance="plain" href="/hassio/update-available/${key}">
${this.supervisor.localize("common.show")}
</ha-button>
</div>
</ha-card>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
.icon {
--mdc-icon-size: 48px;
float: right;
margin: 0 0 2px 10px;
color: var(--primary-text-color);
}
.update-heading {
font-size: var(--ha-font-size-l);
font-weight: var(--ha-font-weight-medium);
margin-bottom: 0.5em;
color: var(--primary-text-color);
}
.card-content {
height: calc(100% - 47px);
box-sizing: border-box;
}
.card-actions {
text-align: right;
}
a {
text-decoration: none;
}
ha-settings-row {
padding: 0;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-update": HassioUpdate;
}
}
@@ -1,155 +0,0 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../src/components/ha-form/types";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { changeMountOptions } from "../../../../src/data/supervisor/mounts";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import type { HassioBackupLocationDialogParams } from "./show-dialog-hassio-backu-location";
const SCHEMA = memoizeOne(
() =>
[
{
name: "default_backup_mount",
required: true,
selector: { backup_location: {} },
},
] as const
);
@customElement("dialog-hassio-backup-location")
class HassioBackupLocationDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _dialogParams?: HassioBackupLocationDialogParams;
@state() private _data?: { default_backup_mount: string | null };
@state() private _waiting?: boolean;
@state() private _error?: string;
public async showDialog(
dialogParams: HassioBackupLocationDialogParams
): Promise<void> {
this._dialogParams = dialogParams;
}
public closeDialog(): void {
this._data = undefined;
this._error = undefined;
this._waiting = undefined;
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._dialogParams) {
return nothing;
}
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
.heading=${this._dialogParams.supervisor.localize(
"dialog.backup_location.title"
)}
@closed=${this.closeDialog}
>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<ha-form
.hass=${this.hass}
.data=${this._data}
.schema=${SCHEMA()}
.computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
@value-changed=${this._valueChanged}
dialogInitialFocus
></ha-form>
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
dialogInitialFocus
>
${this._dialogParams.supervisor.localize("common.cancel")}
</ha-button>
<ha-button
.disabled=${this._waiting || !this._data}
slot="primaryAction"
@click=${this._changeMount}
>
${this._dialogParams.supervisor.localize("common.save")}
</ha-button>
</ha-dialog>
`;
}
private _computeLabelCallback = (
// @ts-ignore
schema: SchemaUnion<ReturnType<typeof SCHEMA>>
): string =>
this._dialogParams!.supervisor.localize(
`dialog.backup_location.options.${schema.name}.name`
) || schema.name;
private _computeHelperCallback = (
// @ts-ignore
schema: SchemaUnion<ReturnType<typeof SCHEMA>>
): string =>
this._dialogParams!.supervisor.localize(
`dialog.backup_location.options.${schema.name}.description`
);
private _valueChanged(ev: CustomEvent) {
const newLocation = ev.detail.value.default_backup_mount;
this._data = {
default_backup_mount: newLocation === "/backup" ? null : newLocation,
};
}
private async _changeMount() {
if (!this._data) {
return;
}
this._error = undefined;
this._waiting = true;
try {
await changeMountOptions(this.hass, this._data);
} catch (err: any) {
this._error = extractApiErrorMessage(err);
this._waiting = false;
return;
}
this.closeDialog();
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
.delete-btn {
--mdc-theme-primary: var(--error-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-backup-location": HassioBackupLocationDialog;
}
}
@@ -1,113 +0,0 @@
import { mdiClose } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-dialog";
import type { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import "../../components/hassio-upload-backup";
import type { HassioBackupUploadDialogParams } from "./show-dialog-backup-upload";
@customElement("dialog-hassio-backup-upload")
export class DialogHassioBackupUpload
extends LitElement
implements HassDialog<HassioBackupUploadDialogParams>
{
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _dialogParams?: HassioBackupUploadDialogParams;
public async showDialog(
dialogParams: HassioBackupUploadDialogParams
): Promise<void> {
this._dialogParams = dialogParams;
await this.updateComplete;
}
public closeDialog() {
if (this._dialogParams && !this._dialogParams.onboarding) {
if (this._dialogParams.reloadBackup) {
this._dialogParams.reloadBackup();
}
}
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
return true;
}
protected render() {
if (!this._dialogParams) {
return nothing;
}
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
hideActions
.heading=${this.hass?.localize(
"ui.panel.page-onboarding.restore.upload_backup"
) || "Upload backup"}
@closed=${this.closeDialog}
>
<div slot="heading">
<ha-header-bar>
<span slot="title"
>${this.hass?.localize(
"ui.panel.page-onboarding.restore.upload_backup"
) || "Upload backup"}</span
>
<ha-icon-button
.label=${this.hass?.localize("ui.common.close") || "Close"}
.path=${mdiClose}
slot="actionItems"
dialogAction="cancel"
dialogInitialFocus
></ha-icon-button>
</ha-header-bar>
</div>
<hassio-upload-backup
@hassio-backup-uploaded=${this._backupUploaded}
.hass=${this.hass}
></hassio-upload-backup>
</ha-dialog>
`;
}
private _backupUploaded(ev) {
const backup = ev.detail.backup;
this._dialogParams?.showBackup(backup.slug);
this.closeDialog();
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
}
/* overrule the ha-style-dialog max-height on small screens */
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-header-bar {
--mdc-theme-primary: var(--app-header-background-color);
--mdc-theme-on-primary: var(--app-header-text-color, white);
}
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-backup-upload": DialogHassioBackupUpload;
}
}
@@ -1,339 +0,0 @@
import type { ActionDetail } from "@material/mwc-list";
import { mdiClose, mdiDotsVertical } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { atLeastVersion } from "../../../../src/common/config/version";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { stopPropagation } from "../../../../src/common/dom/stop_propagation";
import { slugify } from "../../../../src/common/string/slugify";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-button-menu";
import "../../../../src/components/ha-dialog-header";
import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-list-item";
import "../../../../src/components/ha-md-dialog";
import type { HaMdDialog } from "../../../../src/components/ha-md-dialog";
import "../../../../src/components/ha-spinner";
import { getSignedPath } from "../../../../src/data/auth";
import type { HassioBackupDetail } from "../../../../src/data/hassio/backup";
import {
fetchHassioBackupInfo,
removeBackup,
restoreBackup,
} from "../../../../src/data/hassio/backup";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../src/dialogs/generic/show-dialog-box";
import type { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { fileDownload } from "../../../../src/util/file_download";
import "../../components/supervisor-backup-content";
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
import type { HassioBackupDialogParams } from "./show-dialog-hassio-backup";
@customElement("dialog-hassio-backup")
class HassioBackupDialog
extends LitElement
implements HassDialog<HassioBackupDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _error?: string;
@state() private _backup?: HassioBackupDetail;
@state() private _dialogParams?: HassioBackupDialogParams;
@state() private _restoringBackup = false;
@query("supervisor-backup-content")
private _backupContent!: SupervisorBackupContent;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public async showDialog(dialogParams: HassioBackupDialogParams) {
this._dialogParams = dialogParams;
this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug);
if (!this._backup) {
this._error = this._dialogParams.supervisor?.localize(
"backup.no_backup_found"
);
} else if (this._dialogParams.onboarding && !this._backup.homeassistant) {
this._error = this._dialogParams.supervisor?.localize(
"backup.restore_no_home_assistant"
);
}
this._restoringBackup = false;
}
private _dialogClosed(): void {
this._backup = undefined;
this._dialogParams = undefined;
this._restoringBackup = false;
this._error = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
public closeDialog() {
this._dialog?.close();
return true;
}
protected render() {
if (!this._dialogParams || !this._backup) {
return nothing;
}
return html`
<ha-md-dialog
open
.disableCancelAction=${!this._error}
@closed=${this._dialogClosed}
>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
.label=${this._dialogParams.supervisor?.localize("backup.close")}
.path=${mdiClose}
@click=${this.closeDialog}
.disabled=${this._restoringBackup}
></ha-icon-button>
<span slot="title" .title=${this._backup.name}
>${this._backup.name}</span
>
${!this._dialogParams.onboarding && this._dialogParams.supervisor
? html`<ha-button-menu
slot="actionItems"
fixed
@action=${this._handleMenuAction}
@closed=${stopPropagation}
>
<ha-icon-button
.label=${this._dialogParams.supervisor.localize(
"backup.more_actions"
)}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<ha-list-item
>${this._dialogParams.supervisor.localize(
"backup.download_backup"
)}</ha-list-item
>
<ha-list-item class="error"
>${this._dialogParams.supervisor.localize(
"backup.delete_backup_title"
)}</ha-list-item
>
</ha-button-menu>`
: nothing}
</ha-dialog-header>
<div slot="content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: this._restoringBackup
? html`<div class="loading">
<ha-spinner></ha-spinner>
</div>`
: html`
<supervisor-backup-content
.hass=${this.hass}
.supervisor=${this._dialogParams.supervisor}
.backup=${this._backup}
.onboarding=${this._dialogParams.onboarding || false}
dialogInitialFocus
>
</supervisor-backup-content>
`}
</div>
<div slot="actions">
<ha-button
.disabled=${this._restoringBackup || !!this._error}
@click=${this._restoreClicked}
>
${this._dialogParams.supervisor?.localize("backup.restore")}
</ha-button>
</div>
</ha-md-dialog>
`;
}
private _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._downloadClicked();
break;
case 1:
this._deleteClicked();
break;
}
}
private async _restoreClicked() {
const backupDetails = this._backupContent.backupDetails();
this._restoringBackup = true;
const supervisor = this._dialogParams?.supervisor;
if (supervisor !== undefined && supervisor.info.state !== "running") {
await showAlertDialog(this, {
title: supervisor.localize("backup.could_not_restore"),
text: supervisor.localize("backup.restore_blocked_not_running", {
state: supervisor.info.state,
}),
});
this._restoringBackup = false;
return;
}
if (
!(await showConfirmationDialog(this, {
title: supervisor?.localize(
`backup.${
this._backup!.type === "full"
? "confirm_restore_full_backup_title"
: "confirm_restore_partial_backup_title"
}`
),
text: supervisor?.localize(
`backup.${
this._backup!.type === "full"
? "confirm_restore_full_backup_text"
: "confirm_restore_partial_backup_text"
}`
),
confirmText: supervisor?.localize("backup.restore"),
dismissText: supervisor?.localize("backup.cancel"),
}))
) {
this._restoringBackup = false;
return;
}
try {
await restoreBackup(
this.hass,
this._backup!.type,
this._backup!.slug,
{ ...backupDetails, background: this._dialogParams?.onboarding },
!!this.hass && atLeastVersion(this.hass.config.version, 2021, 9)
);
this._dialogParams?.onRestoring?.();
this.closeDialog();
} catch (error: any) {
this._error =
error?.body?.message ||
supervisor?.localize("backup.restore_start_failed");
} finally {
this._restoringBackup = false;
}
}
private async _deleteClicked() {
const supervisor = this._dialogParams?.supervisor;
if (!supervisor) return;
if (
!(await showConfirmationDialog(this, {
title: supervisor!.localize("backup.confirm_delete_title"),
text: supervisor!.localize("backup.confirm_delete_text"),
confirmText: supervisor!.localize("backup.delete"),
dismissText: supervisor!.localize("backup.cancel"),
destructive: true,
}))
) {
return;
}
try {
await removeBackup(this.hass!, this._backup!.slug);
if (this._dialogParams!.onDelete) {
this._dialogParams!.onDelete();
}
this.closeDialog();
} catch (err: any) {
this._error = err.body.message;
}
}
private async _downloadClicked() {
const supervisor = this._dialogParams?.supervisor;
if (!supervisor) return;
let signedPath: { path: string };
try {
signedPath = await getSignedPath(
this.hass!,
`/api/hassio/${
atLeastVersion(this.hass!.config.version, 2021, 9)
? "backups"
: "snapshots"
}/${this._backup!.slug}/download`
);
} catch (err: any) {
await showAlertDialog(this, {
text: extractApiErrorMessage(err),
});
return;
}
if (window.location.href.includes("ui.nabu.casa")) {
const confirm = await showConfirmationDialog(this, {
title: supervisor.localize("backup.remote_download_title"),
text: supervisor.localize("backup.remote_download_text"),
confirmText: supervisor.localize("backup.download"),
dismissText: supervisor?.localize("backup.cancel"),
});
if (!confirm) {
return;
}
}
fileDownload(
signedPath.path,
`home_assistant_backup_${slugify(this._computeName)}.tar`
);
}
private get _computeName() {
return this._backup
? this._backup.name || this._backup.slug
: this._dialogParams!.supervisor?.localize("backup.unnamed_backup") || "";
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
display: block;
}
ha-icon-button {
color: var(--secondary-text-color);
}
.loading {
width: 100%;
display: flex;
height: 100%;
justify-content: center;
align-items: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-backup": HassioBackupDialog;
}
}
@@ -1,158 +0,0 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-spinner";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import {
createHassioFullBackup,
createHassioPartialBackup,
} from "../../../../src/data/hassio/backup";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import "../../components/supervisor-backup-content";
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
import type { HassioCreateBackupDialogParams } from "./show-dialog-hassio-create-backup";
@customElement("dialog-hassio-create-backup")
class HassioCreateBackupDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _dialogParams?: HassioCreateBackupDialogParams;
@state() private _error?: string;
@state() private _creatingBackup = false;
@query("supervisor-backup-content")
private _backupContent!: SupervisorBackupContent;
public showDialog(dialogParams: HassioCreateBackupDialogParams) {
this._dialogParams = dialogParams;
this._creatingBackup = false;
}
public closeDialog() {
this._dialogParams = undefined;
this._creatingBackup = false;
this._error = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._dialogParams) {
return nothing;
}
return html`
<ha-dialog
open
scrimClickAction
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this._dialogParams.supervisor.localize("backup.create_backup")
)}
>
${this._creatingBackup
? html`<ha-spinner></ha-spinner>`
: html`<supervisor-backup-content
.hass=${this.hass}
.supervisor=${this._dialogParams.supervisor}
dialogInitialFocus
>
</supervisor-backup-content>`}
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
>
${this._dialogParams.supervisor.localize("common.close")}
</ha-button>
<ha-button
.disabled=${this._creatingBackup}
slot="primaryAction"
@click=${this._createBackup}
>
${this._dialogParams.supervisor.localize("backup.create")}
</ha-button>
</ha-dialog>
`;
}
private async _createBackup(): Promise<void> {
if (this._dialogParams!.supervisor.info.state !== "running") {
showAlertDialog(this, {
title: this._dialogParams!.supervisor.localize(
"backup.could_not_create"
),
text: this._dialogParams!.supervisor.localize(
"backup.create_blocked_not_running",
{ state: this._dialogParams!.supervisor.info.state }
),
});
return;
}
const backupDetails = this._backupContent.backupDetails();
this._creatingBackup = true;
this._error = "";
if (backupDetails.password && !backupDetails.password.length) {
this._error = this._dialogParams!.supervisor.localize(
"backup.enter_password"
);
this._creatingBackup = false;
return;
}
if (
backupDetails.password &&
backupDetails.password !== backupDetails.confirm_password
) {
this._error = this._dialogParams!.supervisor.localize(
"backup.passwords_not_matching"
);
this._creatingBackup = false;
return;
}
delete backupDetails.confirm_password;
try {
if (this._backupContent.backupType === "full") {
await createHassioFullBackup(this.hass, backupDetails);
} else {
await createHassioPartialBackup(this.hass, backupDetails);
}
this._dialogParams!.onCreate();
this.closeDialog();
} catch (err: any) {
this._error = extractApiErrorMessage(err);
}
this._creatingBackup = false;
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
:host {
direction: var(--direction);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-create-backup": HassioCreateBackupDialog;
}
}
@@ -1,19 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "./dialog-hassio-backup-upload";
export interface HassioBackupUploadDialogParams {
showBackup: (slug: string) => void;
reloadBackup?: () => Promise<void>;
onboarding?: boolean;
}
export const showBackupUploadDialog = (
element: HTMLElement,
dialogParams: HassioBackupUploadDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-backup-upload",
dialogImport: () => import("./dialog-hassio-backup-upload"),
dialogParams,
});
};
@@ -1,17 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioBackupLocationDialogParams {
supervisor: Supervisor;
}
export const showHassioBackupLocationDialog = (
element: HTMLElement,
dialogParams: HassioBackupLocationDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-backup-location",
dialogImport: () => import("./dialog-hassio-backup-location"),
dialogParams,
});
};
@@ -1,21 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioBackupDialogParams {
slug: string;
onDelete?: () => void;
onRestoring?: () => void;
onboarding?: boolean;
supervisor?: Supervisor;
}
export const showHassioBackupDialog = (
element: HTMLElement,
dialogParams: HassioBackupDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-backup",
dialogImport: () => import("./dialog-hassio-backup"),
dialogParams,
});
};
@@ -1,18 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioCreateBackupDialogParams {
supervisor: Supervisor;
onCreate: () => void;
}
export const showHassioCreateBackupDialog = (
element: HTMLElement,
dialogParams: HassioCreateBackupDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-create-backup",
dialogImport: () => import("./dialog-hassio-create-backup"),
dialogParams,
});
};
@@ -1,184 +0,0 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-list-item";
import "../../../../src/components/ha-select";
import "../../../../src/components/ha-spinner";
import {
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../../src/data/hassio/common";
import type { DatadiskList } from "../../../../src/data/hassio/host";
import { listDatadisks, moveDatadisk } from "../../../../src/data/hassio/host";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import type { HassioDatatiskDialogParams } from "./show-dialog-hassio-datadisk";
const calculateMoveTime = memoizeOne((supervisor: Supervisor): number => {
// Assume a speed of 30 MB/s.
const moveTime = (supervisor.host.disk_used * 1000) / 60 / 30;
const rebootTime = (supervisor.host.startup_time * 4) / 60;
return Math.ceil((moveTime + rebootTime) / 10) * 10;
});
@customElement("dialog-hassio-datadisk")
class HassioDatadiskDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private dialogParams?: HassioDatatiskDialogParams;
@state() private selectedDevice?: string;
@state() private devices?: DatadiskList["devices"];
@state() private moving = false;
public showDialog(params: HassioDatatiskDialogParams) {
this.dialogParams = params;
listDatadisks(this.hass).then((data) => {
this.devices = data.devices;
});
}
public closeDialog(): void {
this.dialogParams = undefined;
this.selectedDevice = undefined;
this.devices = undefined;
this.moving = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this.dialogParams) {
return nothing;
}
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
.heading=${this.moving
? this.dialogParams.supervisor.localize("dialog.datadisk_move.moving")
: this.dialogParams.supervisor.localize("dialog.datadisk_move.title")}
@closed=${this.closeDialog}
?hideActions=${this.moving}
>
${this.moving
? html`<ha-spinner aria-label="Moving" size="large"></ha-spinner>
<p class="progress-text">
${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.moving_desc"
)}
</p>`
: html` ${this.devices?.length
? html`
${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.description",
{
current_path: this.dialogParams.supervisor.os.data_disk,
time: calculateMoveTime(this.dialogParams.supervisor),
}
)}
<br /><br />
<ha-select
.label=${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.select_device"
)}
@selected=${this._selectDevice}
dialogInitialFocus
>
${this.devices.map(
(device) =>
html`<ha-list-item .value=${device}
>${device}</ha-list-item
>`
)}
</ha-select>
`
: this.devices === undefined
? this.dialogParams.supervisor.localize(
"dialog.datadisk_move.loading_devices"
)
: this.dialogParams.supervisor.localize(
"dialog.datadisk_move.no_devices"
)}
<ha-button
appearance="plain"
slot="primaryAction"
@click=${this.closeDialog}
dialogInitialFocus
>
${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.cancel"
)}
</ha-button>
<ha-button
.disabled=${!this.selectedDevice}
slot="primaryAction"
@click=${this._moveDatadisk}
>
${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.move"
)}
</ha-button>`}
</ha-dialog>
`;
}
private _selectDevice(ev) {
this.selectedDevice = ev.target.value;
}
private async _moveDatadisk() {
this.moving = true;
try {
await moveDatadisk(this.hass, this.selectedDevice!);
} catch (err: any) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: this.dialogParams!.supervisor.localize(
"system.host.failed_to_move"
),
text: extractApiErrorMessage(err),
});
this.closeDialog();
}
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-select {
width: 100%;
}
ha-spinner {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-datadisk": HassioDatadiskDialog;
}
}
@@ -1,17 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioDatatiskDialogParams {
supervisor: Supervisor;
}
export const showHassioDatadiskDialog = (
element: HTMLElement,
dialogParams: HassioDatatiskDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-datadisk",
dialogImport: () => import("./dialog-hassio-datadisk"),
dialogParams,
});
};
@@ -1,199 +0,0 @@
import { mdiClose } from "@mdi/js";
import { dump } from "js-yaml";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { stringCompare } from "../../../../src/common/string/compare";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/search-input";
import type { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import type { HassioHardwareDialogParams } from "./show-dialog-hassio-hardware";
const _filterDevices = memoizeOne(
(hardware: HassioHardwareInfo, filter: string, language: string) =>
hardware.devices
.filter(
(device) =>
device.by_id?.toLowerCase().includes(filter) ||
device.name.toLowerCase().includes(filter) ||
device.dev_path.toLocaleLowerCase().includes(filter) ||
JSON.stringify(device.attributes).toLocaleLowerCase().includes(filter)
)
.sort((a, b) => stringCompare(a.name, b.name, language))
);
@customElement("dialog-hassio-hardware")
class HassioHardwareDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _dialogParams?: HassioHardwareDialogParams;
@state() private _filter?: string;
public showDialog(dialogParams: HassioHardwareDialogParams) {
this._dialogParams = dialogParams;
}
public closeDialog() {
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._dialogParams) {
return nothing;
}
const devices = _filterDevices(
this._dialogParams.hardware,
(this._filter || "").toLowerCase(),
this.hass.locale.language
);
return html`
<ha-dialog
open
scrimClickAction
hideActions
@closed=${this.closeDialog}
.heading=${this._dialogParams.supervisor.localize(
"dialog.hardware.title"
)}
>
<div class="header" slot="heading">
<h2>
${this._dialogParams.supervisor.localize("dialog.hardware.title")}
</h2>
<ha-icon-button
.label=${this._dialogParams.supervisor.localize("common.close")}
.path=${mdiClose}
dialogAction="close"
></ha-icon-button>
<search-input
.hass=${this.hass}
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
.label=${this._dialogParams.supervisor.localize(
"dialog.hardware.search"
)}
>
</search-input>
</div>
${devices.map(
(device) =>
html`<ha-expansion-panel
.header=${device.name}
.secondary=${device.by_id || undefined}
outlined
>
<div class="device-property">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.subsystem"
)}:
</span>
<span>${device.subsystem}</span>
</div>
<div class="device-property">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.device_path"
)}:
</span>
<code>${device.dev_path}</code>
</div>
${device.by_id
? html` <div class="device-property">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.id"
)}:
</span>
<code>${device.by_id}</code>
</div>`
: ""}
<div class="attributes">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.attributes"
)}:
</span>
<pre>${dump(device.attributes, { indent: 2 })}</pre>
</div>
</ha-expansion-panel>`
)}
</ha-dialog>
`;
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-icon-button {
position: absolute;
right: 16px;
inset-inline-end: 16px;
inset-inline-start: initial;
top: 10px;
text-decoration: none;
color: var(--primary-text-color);
}
h2 {
margin: 18px 42px 0 18px;
margin-inline-start: 18px;
margin-inline-end: 42px;
color: var(--primary-text-color);
}
ha-expansion-panel {
margin: 4px 0;
}
pre,
code {
background-color: var(--markdown-code-background-color, none);
border-radius: var(--ha-border-radius-sm);
}
pre {
padding: 16px;
overflow: auto;
line-height: 1.45;
font-family: var(--ha-font-family-code);
}
code {
font-size: var(--ha-font-size-s);
padding: 0.2em 0.4em;
}
search-input {
margin: 8px 16px 0;
display: block;
}
.device-property {
display: flex;
justify-content: space-between;
}
.attributes {
margin-top: 12px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-hardware": HassioHardwareDialog;
}
}
@@ -1,19 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioHardwareDialogParams {
supervisor: Supervisor;
hardware: HassioHardwareInfo;
}
export const showHassioHardwareDialog = (
element: HTMLElement,
dialogParams: HassioHardwareDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-hardware",
dialogImport: () => import("./dialog-hassio-hardware"),
dialogParams,
});
};
@@ -1,70 +0,0 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-markdown";
import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
import type { HassioMarkdownDialogParams } from "./show-dialog-hassio-markdown";
@customElement("dialog-hassio-markdown")
class HassioMarkdownDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@property() public title!: string;
@property() public content!: string;
@state() private _opened = false;
public showDialog(params: HassioMarkdownDialogParams) {
this.title = params.title;
this.content = params.content;
this._opened = true;
}
public closeDialog() {
this._opened = false;
}
protected render() {
if (!this._opened) {
return nothing;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(this.hass, this.title)}
hideactions
>
<ha-markdown
.content=${this.content || ""}
dialogInitialFocus
></ha-markdown>
</ha-dialog>
`;
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
hassioStyle,
css`
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-markdown {
padding: 16px;
}
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-markdown": HassioMarkdownDialog;
}
}
@@ -1,17 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
export interface HassioMarkdownDialogParams {
title: string;
content: string;
}
export const showHassioMarkdownDialog = (
element: HTMLElement,
dialogParams: HassioMarkdownDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-markdown",
dialogImport: () => import("./dialog-hassio-markdown"),
dialogParams,
});
};
@@ -1,647 +0,0 @@
import { mdiClose } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-list";
import "../../../../src/components/ha-list-item";
import "../../../../src/components/ha-password-field";
import "../../../../src/components/ha-radio";
import "../../../../src/components/ha-tab-group";
import "../../../../src/components/ha-tab-group-tab";
import "../../../../src/components/ha-textfield";
import type { HaTextField } from "../../../../src/components/ha-textfield";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import type {
AccessPoints,
NetworkInterface,
WifiConfiguration,
} from "../../../../src/data/hassio/network";
import {
accesspointScan,
updateNetworkInterface,
} from "../../../../src/data/hassio/network";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../src/dialogs/generic/show-dialog-box";
import type { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import type { HassioNetworkDialogParams } from "./show-dialog-network";
const IP_VERSIONS = ["ipv4", "ipv6"];
@customElement("dialog-hassio-network")
export class DialogHassioNetwork
extends LitElement
implements HassDialog<HassioNetworkDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@state() private _accessPoints?: AccessPoints;
@state() private _curTabIndex = 0;
@state() private _dirty = false;
@state() private _interface?: NetworkInterface;
@state() private _interfaces!: NetworkInterface[];
@state() private _params?: HassioNetworkDialogParams;
@state() private _processing = false;
@state() private _scanning = false;
@state() private _wifiConfiguration?: WifiConfiguration;
public async showDialog(params: HassioNetworkDialogParams): Promise<void> {
this._params = params;
this._dirty = false;
this._curTabIndex = 0;
this.supervisor = params.supervisor;
this._interfaces = params.supervisor.network.interfaces.sort((a, b) =>
a.primary > b.primary ? -1 : 1
);
this._interface = { ...this._interfaces[this._curTabIndex] };
await this.updateComplete;
}
public closeDialog() {
this._params = undefined;
this._processing = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
return true;
}
protected render() {
if (!this._params || !this._interface) {
return nothing;
}
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
.heading=${this.supervisor.localize("dialog.network.title")}
hideActions
@closed=${this.closeDialog}
>
<div slot="heading">
<ha-header-bar>
<span slot="title">
${this.supervisor.localize("dialog.network.title")}
</span>
<ha-icon-button
.label=${this.supervisor.localize("common.close")}
.path=${mdiClose}
slot="actionItems"
dialogAction="cancel"
></ha-icon-button>
</ha-header-bar>
${this._interfaces.length > 1
? html`<ha-tab-group @wa-tab-show=${this._handleTabActivated}
>${this._interfaces.map(
(device, index) =>
html`<ha-tab-group-tab
slot="nav"
.id=${device.interface}
.panel=${index.toString()}
.active=${this._curTabIndex === index}
>
${device.interface}
</ha-tab-group-tab>`
)}
</ha-tab-group>`
: ""}
</div>
${cache(this._renderTab())}
</ha-dialog>
`;
}
private _renderTab() {
return html` <div class="form container">
${IP_VERSIONS.map((version) =>
this._interface![version] ? this._renderIPConfiguration(version) : ""
)}
${this._interface?.type === "wireless"
? html`
<ha-expansion-panel
.header=${this.supervisor.localize("dialog.network.wifi")}
outlined
>
${this._interface?.wifi?.ssid
? html`<p>
${this.supervisor.localize(
"dialog.network.connected_to",
{ ssid: this._interface?.wifi?.ssid }
)}
</p>`
: ""}
<ha-button
appearance="plain"
size="small"
class="scan"
@click=${this._scanForAP}
.disabled=${this._scanning}
.loading=${this._scanning}
>
${this.supervisor.localize("dialog.network.scan_ap")}
</ha-button>
${this._accessPoints &&
this._accessPoints.accesspoints &&
this._accessPoints.accesspoints.length !== 0
? html`
<ha-list>
${this._accessPoints.accesspoints
.filter((ap) => ap.ssid)
.map(
(ap) => html`
<ha-list-item
twoline
@click=${this._selectAP}
.activated=${ap.ssid ===
this._wifiConfiguration?.ssid}
.ap=${ap}
>
<span>${ap.ssid}</span>
<span slot="secondary">
${ap.mac} -
${this.supervisor.localize(
"dialog.network.signal_strength"
)}:
${ap.signal}
</span>
</ha-list-item>
`
)}
</ha-list>
`
: ""}
${this._wifiConfiguration
? html`
<div class="radio-row">
<ha-formfield
.label=${this.supervisor.localize(
"dialog.network.open"
)}
>
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
value="open"
name="auth"
.checked=${this._wifiConfiguration.auth ===
undefined ||
this._wifiConfiguration.auth === "open"}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.supervisor.localize(
"dialog.network.wep"
)}
>
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
value="wep"
name="auth"
.checked=${this._wifiConfiguration.auth === "wep"}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.supervisor.localize(
"dialog.network.wpa"
)}
>
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
value="wpa-psk"
name="auth"
.checked=${this._wifiConfiguration.auth ===
"wpa-psk"}
>
</ha-radio>
</ha-formfield>
</div>
${this._wifiConfiguration.auth === "wpa-psk" ||
this._wifiConfiguration.auth === "wep"
? html`
<ha-password-field
class="flex-auto"
id="psk"
.label=${this.supervisor.localize(
"dialog.network.wifi_password"
)}
version="wifi"
@change=${this._handleInputValueChangedWifi}
>
</ha-password-field>
`
: ""}
`
: ""}
</ha-expansion-panel>
`
: ""}
${this._dirty
? html`<ha-alert alert-type="warning">
${this.supervisor.localize("dialog.network.warning")}
</ha-alert>`
: ""}
</div>
<div class="buttons">
<ha-button @click=${this.closeDialog} appearance="plain">
${this.supervisor.localize("common.cancel")}
</ha-button>
<ha-button
@click=${this._updateNetwork}
.disabled=${!this._dirty}
.loading=${this._processing}
>
${this.supervisor.localize("common.save")}
</ha-button>
</div>`;
}
private _selectAP(event) {
this._wifiConfiguration = event.currentTarget.ap;
this._dirty = true;
}
private async _scanForAP() {
if (!this._interface) {
return;
}
this._scanning = true;
try {
this._accessPoints = await accesspointScan(
this.hass,
this._interface.interface
);
} catch (err: any) {
showAlertDialog(this, {
title: "Failed to scan for accesspoints",
text: extractApiErrorMessage(err),
});
} finally {
this._scanning = false;
}
}
private _renderIPConfiguration(version: string) {
return html`
<ha-expansion-panel
.header=${`IPv${version.charAt(version.length - 1)}`}
outlined
>
<div class="radio-row">
<ha-formfield
.label=${this.supervisor.localize("dialog.network.auto")}
>
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
value="auto"
name="${version}method"
.checked=${this._interface![version]?.method === "auto"}
dialogInitialFocus
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.supervisor.localize("dialog.network.static")}
>
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
value="static"
name="${version}method"
.checked=${this._interface![version]?.method === "static"}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.supervisor.localize("dialog.network.disabled")}
class="warning"
>
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
value="disabled"
name="${version}method"
.checked=${this._interface![version]?.method === "disabled"}
>
</ha-radio>
</ha-formfield>
</div>
${this._interface![version].method === "static"
? html`
<ha-textfield
class="flex-auto"
id="address"
.label=${this.supervisor.localize("dialog.network.ip_netmask")}
.version=${version}
.value=${this._toString(this._interface![version].address)}
@change=${this._handleInputValueChanged}
>
</ha-textfield>
<ha-textfield
class="flex-auto"
id="gateway"
.label=${this.supervisor.localize("dialog.network.gateway")}
.version=${version}
.value=${this._interface![version].gateway}
@change=${this._handleInputValueChanged}
>
</ha-textfield>
<ha-textfield
class="flex-auto"
id="nameservers"
.label=${this.supervisor.localize("dialog.network.dns_servers")}
.version=${version}
.value=${this._toString(this._interface![version].nameservers)}
@change=${this._handleInputValueChanged}
>
</ha-textfield>
`
: ""}
</ha-expansion-panel>
`;
}
private _toArray(data: string | string[]): string[] {
if (Array.isArray(data)) {
if (data && typeof data[0] === "string") {
data = data[0];
}
}
if (!data) {
return [];
}
if (typeof data === "string") {
return data.replace(/ /g, "").split(",");
}
return data;
}
private _toString(data: string | string[]): string {
if (!data) {
return "";
}
if (Array.isArray(data)) {
return data.join(", ");
}
return data;
}
private async _updateNetwork() {
this._processing = true;
let interfaceOptions: Partial<NetworkInterface> = {};
IP_VERSIONS.forEach((version) => {
interfaceOptions[version] = {
method: this._interface![version]?.method || "auto",
};
if (this._interface![version]?.method === "static") {
interfaceOptions[version] = {
...interfaceOptions[version],
address: this._toArray(this._interface![version]?.address),
gateway: this._interface![version]?.gateway,
nameservers: this._toArray(this._interface![version]?.nameservers),
};
}
});
if (this._wifiConfiguration) {
interfaceOptions = {
...interfaceOptions,
wifi: {
ssid: this._wifiConfiguration.ssid,
mode: this._wifiConfiguration.mode,
auth: this._wifiConfiguration.auth || "open",
},
};
if (interfaceOptions.wifi!.auth !== "open") {
interfaceOptions.wifi = {
...interfaceOptions.wifi,
psk: this._wifiConfiguration.psk,
};
}
}
interfaceOptions.enabled =
this._wifiConfiguration !== undefined ||
interfaceOptions.ipv4?.method !== "disabled" ||
interfaceOptions.ipv6?.method !== "disabled";
try {
await updateNetworkInterface(
this.hass,
this._interface!.interface,
interfaceOptions
);
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize("dialog.network.failed_to_change"),
text: extractApiErrorMessage(err),
});
this._processing = false;
return;
}
this._params?.loadData();
this.closeDialog();
}
private async _handleTabActivated(ev: CustomEvent): Promise<void> {
if (this._dirty) {
const confirm = await showConfirmationDialog(this, {
text: this.supervisor.localize("dialog.network.unsaved"),
confirmText: this.supervisor.localize("common.yes"),
dismissText: this.supervisor.localize("common.no"),
});
if (!confirm) {
this.requestUpdate("_interface");
return;
}
}
this._curTabIndex = Number(ev.detail.name);
this._interface = { ...this._interfaces[this._curTabIndex] };
}
private _handleRadioValueChanged(ev: CustomEvent): void {
const value = (ev.target as any).value as "disabled" | "auto" | "static";
const version = (ev.target as any).version as "ipv4" | "ipv6";
if (
!value ||
!this._interface ||
this._interface[version]!.method === value
) {
return;
}
this._dirty = true;
this._interface[version]!.method = value;
this.requestUpdate("_interface");
}
private _handleRadioValueChangedAp(ev: CustomEvent): void {
const value = (ev.target as any).value as string as
| "open"
| "wep"
| "wpa-psk";
this._wifiConfiguration!.auth = value;
this._dirty = true;
this.requestUpdate("_wifiConfiguration");
}
private _handleInputValueChanged(ev: Event): void {
const source = ev.target as HaTextField;
const value = source.value;
const version = (ev.target as any).version as "ipv4" | "ipv6";
const id = source.id;
if (
!value ||
!this._interface ||
this._toString(this._interface[version]![id]) === this._toString(value)
) {
return;
}
this._dirty = true;
this._interface[version]![id] = value;
}
private _handleInputValueChangedWifi(ev: Event): void {
const source = ev.target as HaTextField;
const value = source.value;
const id = source.id;
if (
!value ||
!this._wifiConfiguration ||
this._wifiConfiguration![id] === value
) {
return;
}
this._dirty = true;
this._wifiConfiguration![id] = value;
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
}
ha-dialog {
--dialog-content-position: static;
--dialog-content-padding: 0;
--dialog-z-index: 6;
}
@media all and (min-width: 451px) and (min-height: 501px) {
.container {
width: 400px;
}
}
.content {
display: block;
padding: 20px 24px;
}
/* overrule the ha-style-dialog max-height on small screens */
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-header-bar {
--mdc-theme-primary: var(--app-header-background-color);
--mdc-theme-on-primary: var(--app-header-text-color, white);
}
}
ha-button.scan {
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
}
.container {
padding: 0 8px 4px;
}
.form {
margin-bottom: 53px;
}
.buttons {
position: absolute;
bottom: 0;
width: 100%;
box-sizing: border-box;
border-top: 1px solid
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
display: flex;
justify-content: space-between;
padding: 16px;
padding-bottom: max(var(--safe-area-inset-bottom), 16px);
background-color: var(--mdc-theme-surface, #fff);
}
.warning {
color: var(--error-color);
--primary-color: var(--error-color);
}
div.warning {
margin: 12px 4px -12px;
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0 16px;
margin: 4px 0;
}
ha-textfield {
padding: 0 14px;
}
ha-list-item {
--mdc-list-side-padding: 10px;
}
ha-tab-group-tab {
flex: 1;
}
ha-tab-group-tab::part(base) {
width: 100%;
justify-content: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-network": DialogHassioNetwork;
}
}
@@ -1,19 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import "./dialog-hassio-network";
export interface HassioNetworkDialogParams {
supervisor: Supervisor;
loadData: () => Promise<void>;
}
export const showNetworkDialog = (
element: HTMLElement,
dialogParams: HassioNetworkDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-network",
dialogImport: () => import("./dialog-hassio-network"),
dialogParams,
});
};
@@ -1,255 +0,0 @@
import { mdiDelete, mdiPlus } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-button";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../src/components/ha-form/types";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
addHassioDockerRegistry,
fetchHassioDockerRegistries,
removeHassioDockerRegistry,
} from "../../../../src/data/hassio/docker";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import type { RegistriesDialogParams } from "./show-dialog-registries";
const SCHEMA = [
{
name: "registry",
required: true,
selector: { text: {} },
},
{
name: "username",
required: true,
selector: { text: {} },
},
{
name: "password",
required: true,
selector: { text: { type: "password" } },
},
] as const;
@customElement("dialog-hassio-registries")
class HassioRegistriesDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@state() private _registries?: {
registry: string;
username: string;
}[];
@state() private _input: {
registry?: string;
username?: string;
password?: string;
} = {};
@state() private _opened = false;
@state() private _addingRegistry = false;
protected render(): TemplateResult {
return html`
<ha-dialog
.open=${this._opened}
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
hideActions
.heading=${createCloseHeading(
this.hass,
this._addingRegistry
? this.supervisor.localize("dialog.registries.title_add")
: this.supervisor.localize("dialog.registries.title_manage")
)}
>
${this._addingRegistry
? html`
<ha-form
.data=${this._input}
.schema=${SCHEMA}
@value-changed=${this._valueChanged}
.computeLabel=${this._computeLabel}
dialogInitialFocus
></ha-form>
<div class="action">
<ha-button
?disabled=${Boolean(
!this._input.registry ||
!this._input.username ||
!this._input.password
)}
@click=${this._addNewRegistry}
appearance="filled"
size="small"
>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.supervisor.localize("dialog.registries.add_registry")}
</ha-button>
</div>
`
: html`${this._registries?.length
? this._registries.map(
(entry) => html`
<ha-settings-row class="registry">
<span slot="heading"> ${entry.registry} </span>
<span slot="description">
${this.supervisor.localize(
"dialog.registries.username"
)}:
${entry.username}
</span>
<ha-icon-button
.entry=${entry}
.label=${this.supervisor.localize(
"dialog.registries.remove"
)}
.path=${mdiDelete}
@click=${this._removeRegistry}
></ha-icon-button>
</ha-settings-row>
`
)
: html`
<ha-alert>
${this.supervisor.localize(
"dialog.registries.no_registries"
)}
</ha-alert>
`}
<div class="action">
<ha-button
@click=${this._addRegistry}
dialogInitialFocus
appearance="filled"
size="small"
>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.supervisor.localize(
"dialog.registries.add_new_registry"
)}
</ha-button>
</div> `}
</ha-dialog>
`;
}
private _computeLabel = (schema: SchemaUnion<typeof SCHEMA>) =>
this.supervisor.localize(`dialog.registries.${schema.name}`);
private _valueChanged(ev: CustomEvent) {
this._input = ev.detail.value;
}
public async showDialog(dialogParams: RegistriesDialogParams): Promise<void> {
this._opened = true;
this._input = {};
this.supervisor = dialogParams.supervisor;
await this._loadRegistries();
await this.updateComplete;
}
public closeDialog(): void {
this._addingRegistry = false;
this._opened = false;
this._input = {};
}
public focus(): void {
this.updateComplete.then(() =>
(
this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
)?.focus()
);
}
private async _loadRegistries(): Promise<void> {
const registries = await fetchHassioDockerRegistries(this.hass);
this._registries = Object.keys(registries!.registries).map((key) => ({
registry: key,
username: registries.registries[key].username,
}));
}
private _addRegistry(): void {
this._addingRegistry = true;
}
private async _addNewRegistry(): Promise<void> {
const data = {};
data[this._input.registry!] = {
username: this._input.username,
password: this._input.password,
};
try {
await addHassioDockerRegistry(this.hass, data);
await this._loadRegistries();
this._addingRegistry = false;
this._input = {};
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize("dialog.registries.failed_to_add"),
text: extractApiErrorMessage(err),
});
}
}
private async _removeRegistry(ev: Event): Promise<void> {
const entry = (ev.currentTarget as any).entry;
try {
await removeHassioDockerRegistry(this.hass, entry.registry);
await this._loadRegistries();
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize("dialog.registries.failed_to_remove"),
text: extractApiErrorMessage(err),
});
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
.registry {
border: 1px solid var(--divider-color);
border-radius: var(--ha-border-radius-sm);
margin-top: 4px;
}
.action {
margin-top: 24px;
width: 100%;
display: flex;
justify-content: flex-end;
}
ha-icon-button {
color: var(--error-color);
margin-right: -10px;
margin-inline-end: -10px;
margin-inline-start: initial;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-registries": HassioRegistriesDialog;
}
}
@@ -1,18 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import "./dialog-hassio-registries";
export interface RegistriesDialogParams {
supervisor: Supervisor;
}
export const showRegistriesDialog = (
element: HTMLElement,
dialogParams: RegistriesDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-registries",
dialogImport: () => import("./dialog-hassio-registries"),
dialogParams,
});
};
@@ -1,278 +0,0 @@
import { mdiDelete, mdiDeleteOff, mdiPlus } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../../../../src/common/string/compare";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-md-list";
import "../../../../src/components/ha-md-list-item";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-textfield";
import type { HaTextField } from "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-tooltip";
import type {
HassioAddonInfo,
HassioAddonRepository,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
addStoreRepository,
fetchStoreRepositories,
removeStoreRepository,
} from "../../../../src/data/supervisor/store";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import type { HassioRepositoryDialogParams } from "./show-dialog-repositories";
@customElement("dialog-hassio-repositories")
class HassioRepositoriesDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@query("#repository_input", true) private _optionInput?: HaTextField;
@state() private _repositories?: HassioAddonRepository[];
@state() private _dialogParams?: HassioRepositoryDialogParams;
@state() private _opened = false;
@state() private _processing = false;
@state() private _error?: string;
public async showDialog(
dialogParams: HassioRepositoryDialogParams
): Promise<void> {
this._dialogParams = dialogParams;
this._opened = true;
await this._loadData();
await this.updateComplete;
}
public closeDialog(): void {
this._dialogParams = undefined;
this._opened = false;
this._error = "";
}
private _filteredRepositories = memoizeOne((repos: HassioAddonRepository[]) =>
repos
.filter(
(repo) =>
repo.slug !== "core" && // The core apps repository
repo.slug !== "local" && // Locally managed apps
repo.slug !== "a0d7b954" && // Home Assistant Community Apps
repo.slug !== "5c53de3b" && // The ESPHome repository
repo.slug !== "d5369777" // Music Assistant repository
)
.sort((a, b) =>
caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language)
)
);
private _filteredUsedRepositories = memoizeOne(
(repos: HassioAddonRepository[], addons: HassioAddonInfo[]) =>
repos
.filter((repo) =>
addons.some((addon) => addon.repository === repo.slug)
)
.map((repo) => repo.slug)
);
protected render() {
if (!this._dialogParams?.supervisor || this._repositories === undefined) {
return nothing;
}
const repositories = this._filteredRepositories(this._repositories);
const usedRepositories = this._filteredUsedRepositories(
repositories,
this._dialogParams.supervisor.addon.addons
);
return html`
<ha-dialog
.open=${this._opened}
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this._dialogParams!.supervisor.localize("dialog.repositories.title")
)}
>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="form">
<ha-md-list>
${repositories.length
? repositories.map(
(repo) => html`
<ha-md-list-item class="option">
${repo.name}
<div slot="supporting-text">
<div>${repo.maintainer}</div>
<div>${repo.url}</div>
</div>
<ha-tooltip
.for="icon-button-${repo.slug}"
class="delete"
slot="end"
>
${this._dialogParams!.supervisor.localize(
usedRepositories.includes(repo.slug)
? "dialog.repositories.used"
: "dialog.repositories.remove"
)}
</ha-tooltip>
<div .id="icon-button-${repo.slug}">
<ha-icon-button
.disabled=${usedRepositories.includes(repo.slug)}
.slug=${repo.slug}
.path=${usedRepositories.includes(repo.slug)
? mdiDeleteOff
: mdiDelete}
@click=${this._removeRepository}
>
</ha-icon-button>
</div>
</ha-md-list-item>
`
)
: html`<ha-md-list-item
>${this._dialogParams!.supervisor.localize(
"dialog.repositories.no_repositories"
)}</ha-md-list-item
>`}
</ha-md-list>
<div class="layout horizontal bottom">
<ha-textfield
class="flex-auto"
id="repository_input"
.value=${this._dialogParams!.url || ""}
.label=${this._dialogParams!.supervisor.localize(
"dialog.repositories.add"
)}
@keydown=${this._handleKeyAdd}
dialogInitialFocus
></ha-textfield>
<ha-button
.loading=${this._processing}
@click=${this._addRepository}
appearance="filled"
size="small"
>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this._dialogParams!.supervisor.localize(
"dialog.repositories.add"
)}
</ha-button>
</div>
</div>
<ha-button slot="primaryAction" @click=${this.closeDialog}>
${this._dialogParams?.supervisor.localize("common.close")}
</ha-button>
</ha-dialog>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-dialog.button-left {
--justify-action-buttons: flex-start;
}
.form {
color: var(--primary-text-color);
}
.option {
border: 1px solid var(--divider-color);
border-radius: var(--ha-border-radius-sm);
margin-top: 4px;
}
ha-button {
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
}
div.delete ha-icon-button {
color: var(--error-color);
}
ha-md-list-item {
position: relative;
--md-item-overflow: visible;
}
`,
];
}
public focus() {
this.updateComplete.then(() =>
(
this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
)?.focus()
);
}
private _handleKeyAdd(ev: KeyboardEvent) {
ev.stopPropagation();
if (ev.key !== "Enter") {
return;
}
this._addRepository();
}
private async _loadData(): Promise<void> {
try {
this._repositories = await fetchStoreRepositories(this.hass);
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
} catch (err: any) {
this._error = extractApiErrorMessage(err);
}
}
private async _addRepository() {
const input = this._optionInput;
if (!input || !input.value) {
return;
}
this._processing = true;
try {
await addStoreRepository(this.hass, input.value);
await this._loadData();
fireEvent(this, "supervisor-collection-refresh", { collection: "store" });
input.value = "";
} catch (err: any) {
this._error = extractApiErrorMessage(err);
}
this._processing = false;
}
private async _removeRepository(ev: Event) {
const slug = (ev.currentTarget as any).slug;
try {
await removeStoreRepository(this.hass, slug);
await this._loadData();
fireEvent(this, "supervisor-collection-refresh", { collection: "store" });
} catch (err: any) {
this._error = extractApiErrorMessage(err);
}
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-repositories": HassioRepositoriesDialog;
}
}
@@ -1,19 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import "./dialog-hassio-repositories";
export interface HassioRepositoryDialogParams {
supervisor: Supervisor;
url?: string;
}
export const showRepositoriesDialog = (
element: HTMLElement,
dialogParams: HassioRepositoryDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-repositories",
dialogImport: () => import("./dialog-hassio-repositories"),
dialogParams,
});
};
-38
View File
@@ -1,38 +0,0 @@
import type { LitElement } from "lit";
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
import { restartHassioAddon } from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../src/types";
export const suggestAddonRestart = async (
element: LitElement,
hass: HomeAssistant,
supervisor: Supervisor,
addon: HassioAddonDetails
): Promise<void> => {
const confirmed = await showConfirmationDialog(element, {
title: supervisor.localize("dialog.restart_app.title", {
name: addon.name,
}),
text: supervisor.localize("dialog.restart_app.text"),
confirmText: supervisor.localize("dialog.restart_app.restart"),
dismissText: supervisor.localize("common.cancel"),
});
if (confirmed) {
try {
await restartHassioAddon(hass, addon.slug);
} catch (err: any) {
showAlertDialog(element, {
title: supervisor.localize("common.failed_to_restart_name", {
name: addon.name,
}),
text: extractApiErrorMessage(err),
});
}
}
};
@@ -1,190 +0,0 @@
import { mdiClose, mdiPuzzle, mdiSwapHorizontal } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { atLeastVersion } from "../../../../src/common/config/version";
import "../../../../src/components/ha-dialog-header";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-icon-next";
import "../../../../src/components/ha-md-dialog";
import type { HaMdDialog } from "../../../../src/components/ha-md-dialog";
import "../../../../src/components/ha-md-list";
import "../../../../src/components/ha-md-list-item";
import "../../../../src/components/ha-svg-icon";
import {
getConfigEntry,
type ConfigEntry,
} from "../../../../src/data/config_entries";
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { mdiHomeAssistant } from "../../../../src/resources/home-assistant-logo-svg";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { brandsUrl } from "../../../../src/util/brands-url";
import type { SystemManagedDialogParams } from "./show-dialog-system-managed";
@customElement("dialog-system-managed")
class HassioSystemManagedDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _supervisor?: Supervisor;
@state() private _addon?: HassioAddonDetails;
@state() private _open = false;
@state() private _configEntry?: ConfigEntry;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public async showDialog(
dialogParams: SystemManagedDialogParams
): Promise<void> {
this._addon = dialogParams.addon;
this._supervisor = dialogParams.supervisor;
this._open = true;
this._loadConfigEntry();
}
private _dialogClosed() {
this._addon = undefined;
this._supervisor = undefined;
this._configEntry = undefined;
this._open = false;
}
public closeDialog() {
this._dialog?.close();
return true;
}
protected render() {
if (!this._addon || !this._open || !this._supervisor) {
return nothing;
}
const addonImage =
atLeastVersion(this.hass.config.version, 0, 105) && this._addon.icon
? `/api/hassio/addons/${this._addon.slug}/icon`
: undefined;
return html`
<ha-md-dialog open @closed=${this._dialogClosed}>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>
<span slot="title">${this._addon?.name}</span>
</ha-dialog-header>
<div slot="content">
<div class="icons">
<ha-svg-icon
class="primary"
.path=${mdiHomeAssistant}
></ha-svg-icon>
<ha-svg-icon .path=${mdiSwapHorizontal}></ha-svg-icon>
${addonImage
? html`<img src=${addonImage} alt=${this._addon.name} />`
: html`<ha-svg-icon .path=${mdiPuzzle}></ha-svg-icon>`}
</div>
${this._supervisor.localize("app.system_managed.title")}.<br />
${this._supervisor.localize("app.system_managed.description")}
${this._configEntry
? html`
<h3>
${this._supervisor.localize("app.system_managed.managed_by")}:
</h3>
<ha-md-list>
<ha-md-list-item
type="link"
href=${`/config/integrations/integration/${this._configEntry.domain}`}
>
<img
slot="start"
class="integration-icon"
alt=${this._configEntry.title}
src=${brandsUrl({
domain: this._configEntry.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
${this._configEntry.title}
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
</ha-md-list>
`
: nothing}
</div>
</ha-md-dialog>
`;
}
private _onImageLoad(ev) {
ev.target.style.visibility = "initial";
}
private _onImageError(ev) {
ev.target.style.visibility = "hidden";
}
private async _loadConfigEntry() {
if (this._addon?.system_managed_config_entry) {
try {
const { config_entry } = await getConfigEntry(
this.hass,
this._addon.system_managed_config_entry
);
this._configEntry = config_entry;
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
}
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.icons {
display: flex;
justify-content: center;
align-items: center;
gap: var(--ha-space-4);
--mdc-icon-size: 48px;
margin-bottom: 32px;
}
.icons img {
width: 48px;
}
.icons .primary {
color: var(--primary-color);
}
.actions {
display: flex;
justify-content: space-between;
}
.integration-icon {
width: 24px;
}
ha-md-list-item {
--md-list-item-leading-space: 4px;
--md-list-item-trailing-space: 4px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-system-managed": HassioSystemManagedDialog;
}
}
@@ -1,19 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface SystemManagedDialogParams {
addon: HassioAddonDetails;
supervisor: Supervisor;
}
export const showSystemManagedDialog = (
element: HTMLElement,
dialogParams: SystemManagedDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-system-managed",
dialogImport: () => import("./dialog-system-managed"),
dialogParams,
});
};
-23
View File
@@ -1,23 +0,0 @@
(function () {
function loadES5(src) {
var el = document.createElement("script");
el.src = src;
document.body.appendChild(el);
}
if (<%= modernRegex %>.test(navigator.userAgent)) {
try {
<% for (const entry of latestEntryJS) { %>
new Function("import('<%= entry %>')")();
<% } %>
} catch (err) {
<% for (const entry of es5EntryJS) { %>
loadES5("<%= entry %>");
<% } %>
}
} else {
<% for (const entry of es5EntryJS) { %>
loadES5("<%= entry %>");
<% } %>
}
})();
-28
View File
@@ -1,28 +0,0 @@
import {
haFontFamilyBody,
haFontSmoothing,
haMozOsxFontSmoothing,
} from "../../src/resources/theme/typography.globals";
import "./hassio-main";
import("../../src/resources/append-ha-style");
const styleEl = document.createElement("style");
styleEl.textContent = `
body {
font-family: ${haFontFamilyBody};
-moz-osx-font-smoothing: ${haMozOsxFontSmoothing};
-webkit-font-smoothing: ${haFontSmoothing};
font-weight: var(--ha-font-weight-normal);
margin: 0;
padding: 0;
height: 100vh;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #111111;
color: #e1e1e1;
}
}
`;
document.head.appendChild(styleEl);
-146
View File
@@ -1,146 +0,0 @@
import type { PropertyValues } from "lit";
import { html } from "lit";
import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../src/common/config/version";
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../src/common/dom/fire_event";
import { mainWindow } from "../../src/common/dom/get_main_window";
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
import { navigate } from "../../src/common/navigate";
import type { HassioPanelInfo } from "../../src/data/hassio/supervisor";
import type { Supervisor } from "../../src/data/supervisor/supervisor";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import type { HomeAssistant } from "../../src/types";
import "./hassio-router";
import { SupervisorBaseElement } from "./supervisor-base-element";
@customElement("hassio-main")
export class HassioMain extends SupervisorBaseElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public panel!: HassioPanelInfo;
@property({ type: Boolean }) public narrow = false;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._applyTheme();
// Paulus - March 17, 2019
// We went to a single hass-toggle-menu event in HA 0.90. However, the
// supervisor UI can also run under older versions of Home Assistant.
// So here we are going to translate toggle events into the appropriate
// open and close events. These events are a no-op in newer versions of
// Home Assistant.
this.addEventListener("hass-toggle-menu", () => {
fireEvent(
(window.parent as any).customPanel,
// @ts-ignore
this.hass.dockedSidebar ? "hass-close-menu" : "hass-open-menu"
);
});
// Paulus - March 19, 2019
// We changed the navigate event to fire directly on the window, as that's
// where we are listening for it. However, the older panel_custom will
// listen on this element for navigation events, so we need to forward them.
// Joakim - April 26, 2021
// Due to changes in behavior in Google Chrome, we changed navigate to listen on the top element
mainWindow.addEventListener("location-changed", (ev) =>
// @ts-ignore
fireEvent(this, ev.type, ev.detail, {
bubbles: false,
})
);
// Paulus - May 17, 2021
// Convert the <a> tags to native nav in Home Assistant < 2021.6
document.body.addEventListener("click", (ev) => {
const href = isNavigationClick(ev);
if (href) {
navigate(href);
}
});
// Forward haptic events to parent window.
window.addEventListener("haptic", (ev) => {
// @ts-ignore
fireEvent(window.parent, ev.type, ev.detail, {
bubbles: false,
});
});
// Forward keydown events to the main window for quickbar access
document.body.addEventListener("keydown", (ev: KeyboardEvent) => {
if (ev.altKey || ev.ctrlKey || ev.shiftKey || ev.metaKey) {
// Ignore if modifier keys are pressed
return;
}
// @ts-ignore
fireEvent(mainWindow, "hass-quick-bar-trigger", ev, {
bubbles: false,
});
});
makeDialogManager(this, this.shadowRoot!);
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass) {
return;
}
if (oldHass.themes !== this.hass.themes) {
this._applyTheme();
}
}
protected render() {
return html`
<hassio-router
.hass=${this.hass}
.supervisor=${this.supervisor}
.route=${this.route}
.panel=${this.panel}
.narrow=${this.narrow}
></hassio-router>
`;
}
private _applyTheme() {
let themeName: string;
let themeSettings: Partial<HomeAssistant["selectedTheme"]> | undefined;
if (atLeastVersion(this.hass.config.version, 0, 114)) {
themeName =
this.hass.selectedTheme?.theme ||
(this.hass.themes.darkMode && this.hass.themes.default_dark_theme
? this.hass.themes.default_dark_theme!
: this.hass.themes.default_theme);
themeSettings = this.hass.selectedTheme;
} else {
themeName =
(this.hass.selectedTheme as unknown as string) ||
this.hass.themes.default_theme;
}
applyThemesOnElement(
this.parentElement,
this.hass.themes,
themeName,
themeSettings,
true
);
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-main": HassioMain;
}
}
-155
View File
@@ -1,155 +0,0 @@
import { sanitizeUrl } from "@braintree/sanitize-url";
import type { TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { navigate } from "../../src/common/navigate";
import {
createSearchParam,
extractSearchParamsObject,
} from "../../src/common/url/search-params";
import type { Supervisor } from "../../src/data/supervisor/supervisor";
import "../../src/layouts/hass-error-screen";
import type {
ParamType,
Redirect,
Redirects,
} from "../../src/panels/my/ha-panel-my";
import type { HomeAssistant, Route } from "../../src/types";
export const REDIRECTS: Redirects = {
supervisor: {
redirect: "/hassio/dashboard",
},
supervisor_logs: {
redirect: "/hassio/system",
},
supervisor_info: {
redirect: "/hassio/system",
},
supervisor_snapshots: {
redirect: "/hassio/backups",
},
supervisor_backups: {
redirect: "/hassio/backups",
},
supervisor_store: {
redirect: "/hassio/store",
},
supervisor_addons: {
redirect: "/hassio/dashboard",
},
supervisor_addon: {
redirect: "/hassio/addon",
params: {
addon: "string",
},
optional_params: {
repository_url: "url",
},
},
supervisor_ingress: {
redirect: "/hassio/ingress",
params: {
addon: "string",
},
},
supervisor_add_addon_repository: {
redirect: "/hassio/store",
params: {
repository_url: "url",
},
},
};
@customElement("hassio-my-redirect")
class HassioMyRedirect extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public route!: Route;
@state() public _error?: TemplateResult | string;
connectedCallback() {
super.connectedCallback();
const path = this.route.path.substr(1);
const redirect = REDIRECTS[path];
if (!redirect) {
this._error = this.supervisor.localize("my.not_supported", {
link: html`<a
target="_blank"
rel="noreferrer noopener"
href="https://my.home-assistant.io/faq.html#supported-pages"
>
${this.supervisor.localize("my.faq_link")}
</a>`,
});
return;
}
let url: string;
try {
url = this._createRedirectUrl(redirect);
} catch (_err: any) {
this._error = this.supervisor.localize("my.error");
return;
}
navigate(url, { replace: true });
}
protected render() {
if (this._error) {
return html`<hass-error-screen
.error=${this._error}
></hass-error-screen>`;
}
return nothing;
}
private _createRedirectUrl(redirect: Redirect): string {
const params = this._createRedirectParams(redirect);
return `${redirect.redirect}${params}`;
}
private _createRedirectParams(redirect: Redirect): string {
const params = extractSearchParamsObject();
if (!redirect.params && !Object.keys(params).length) {
return "";
}
const resultParams = {};
Object.entries(redirect.params || {}).forEach(([key, type]) => {
if (!params[key] || !this._checkParamType(type, params[key])) {
throw Error();
}
resultParams[key] = params[key];
});
Object.entries(redirect.optional_params || {}).forEach(([key, type]) => {
if (params[key]) {
if (!this._checkParamType(type, params[key])) {
throw Error();
}
resultParams[key] = params[key];
}
});
return `?${createSearchParam(resultParams)}`;
}
private _checkParamType(type: ParamType, value: string) {
if (type === "string") {
return true;
}
if (type === "url") {
return value && value === sanitizeUrl(value);
}
return false;
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-my-redirect": HassioMyRedirect;
}
}
-53
View File
@@ -1,53 +0,0 @@
import { customElement, property } from "lit/decorators";
import type { Supervisor } from "../../src/data/supervisor/supervisor";
import type { RouterOptions } from "../../src/layouts/hass-router-page";
import { HassRouterPage } from "../../src/layouts/hass-router-page";
import type { HomeAssistant, Route } from "../../src/types";
// Don't codesplit it, that way the dashboard always loads fast.
import "./dashboard/hassio-dashboard";
@customElement("hassio-panel-router")
class HassioPanelRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow = false;
protected routerOptions: RouterOptions = {
beforeRender: (page: string) =>
page === "snapshots" ? "backups" : undefined,
routes: {
dashboard: {
tag: "hassio-dashboard",
},
store: {
tag: "hassio-addon-store",
load: () => import("./addon-store/hassio-addon-store"),
},
backups: {
tag: "hassio-backups",
load: () => import("./backups/hassio-backups"),
},
system: {
tag: "hassio-system",
load: () => import("./system/hassio-system"),
},
},
};
protected updatePageEl(el) {
el.hass = this.hass;
el.supervisor = this.supervisor;
el.route = this.route;
el.narrow = this.narrow;
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-panel-router": HassioPanelRouter;
}
}
-55
View File
@@ -1,55 +0,0 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { Supervisor } from "../../src/data/supervisor/supervisor";
import { supervisorCollection } from "../../src/data/supervisor/supervisor";
import "../../src/layouts/hass-loading-screen";
import type { HomeAssistant, Route } from "../../src/types";
import "./hassio-panel-router";
@customElement("hassio-panel")
class HassioPanel extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
protected render(): TemplateResult {
if (!this.hass) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
if (
Object.keys(supervisorCollection).some(
(collection) => !this.supervisor[collection]
)
) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
return html`
<hassio-panel-router
.hass=${this.hass}
.supervisor=${this.supervisor}
.route=${this.route}
.narrow=${this.narrow}
></hassio-panel-router>
`;
}
static styles = css`
:host {
--app-header-background-color: var(--sidebar-background-color);
--app-header-text-color: var(--sidebar-text-color);
--app-header-border-bottom: 1px solid var(--divider-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hassio-panel": HassioPanel;
}
}
-91
View File
@@ -1,91 +0,0 @@
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { HassioPanelInfo } from "../../src/data/hassio/supervisor";
import type { Supervisor } from "../../src/data/supervisor/supervisor";
import type { RouterOptions } from "../../src/layouts/hass-router-page";
import { HassRouterPage } from "../../src/layouts/hass-router-page";
import type { HomeAssistant } from "../../src/types";
// Don't codesplit it, that way the dashboard always loads fast.
import "./hassio-panel";
@customElement("hassio-router")
class HassioRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public panel!: HassioPanelInfo;
@property({ type: Boolean }) public narrow = false;
protected routerOptions: RouterOptions = {
// Hass.io has a page with tabs, so we route all non-matching routes to it.
defaultPage: "dashboard",
beforeRender: (page: string) => {
if (page === "snapshots") {
return "backups";
}
if (page === "dashboard" && this.panel.config?.ingress) {
return "ingress";
}
return undefined;
},
showLoading: true,
routes: {
dashboard: {
tag: "hassio-panel",
cache: true,
},
backups: "dashboard",
store: "dashboard",
system: "dashboard",
"update-available": {
tag: "update-available-dashboard",
load: () => import("./update-available/update-available-dashboard"),
},
addon: {
tag: "hassio-addon-dashboard",
load: () => import("./addon-view/hassio-addon-dashboard"),
},
ingress: {
tag: "hassio-ingress-view",
load: () => import("./ingress-view/hassio-ingress-view"),
},
_my_redirect: {
tag: "hassio-my-redirect",
load: () => import("./hassio-my-redirect"),
},
},
};
protected updatePageEl(el) {
// the tabs page does its own routing so needs full route.
const hassioPanel = el.localName === "hassio-panel";
const ingressPanel = el.localName === "hassio-ingress-view";
const route = hassioPanel
? this.route
: ingressPanel && this.panel.config?.ingress
? this._ingressRoute(this.panel.config?.ingress)
: this.routeTail;
el.hass = this.hass;
el.narrow = this.narrow;
el.route = route;
el.supervisor = this.supervisor;
if (ingressPanel) {
el.ingressPanel = Boolean(this.panel.config?.ingress);
}
}
private _ingressRoute = memoizeOne((ingress: string) => ({
prefix: "/hassio/ingress",
path: `/${ingress}`,
}));
}
declare global {
interface HTMLElementTagNameMap {
"hassio-router": HassioRouter;
}
}
-34
View File
@@ -1,34 +0,0 @@
import {
mdiBackupRestore,
mdiCogs,
mdiPuzzle,
mdiViewDashboard,
} from "@mdi/js";
import { atLeastVersion } from "../../src/common/config/version";
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
import type { HomeAssistant } from "../../src/types";
export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] =>
atLeastVersion(hass.config.version, 2022, 5)
? []
: [
{
translationKey: atLeastVersion(hass.config.version, 2021, 12)
? "panel.apps"
: "panel.dashboard",
path: `/hassio/dashboard`,
iconPath: atLeastVersion(hass.config.version, 2021, 12)
? mdiPuzzle
: mdiViewDashboard,
},
{
translationKey: "panel.backups",
path: `/hassio/backups`,
iconPath: mdiBackupRestore,
},
{
translationKey: "panel.system",
path: `/hassio/system`,
iconPath: mdiCogs,
},
];
@@ -1,377 +0,0 @@
import { mdiMenu } from "@mdi/js";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { goBack, navigate } from "../../../src/common/navigate";
import { extractSearchParam } from "../../../src/common/url/search-params";
import { nextRender } from "../../../src/common/util/render-status";
import "../../../src/components/ha-icon-button";
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
import {
fetchHassioAddonInfo,
startHassioAddon,
} from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import {
createHassioSession,
validateHassioSession,
} from "../../../src/data/hassio/ingress";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
import type { HomeAssistant, Route } from "../../../src/types";
@customElement("hassio-ingress-view")
class HassioIngressView extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public ingressPanel = false;
@property({ type: Boolean }) public narrow = false;
@state() private _addon?: HassioAddonDetails;
@state() private _loadingMessage?: string;
private _sessionKeepAlive?: number;
private _fetchDataTimeout?: number;
public disconnectedCallback() {
super.disconnectedCallback();
if (this._sessionKeepAlive) {
clearInterval(this._sessionKeepAlive);
this._sessionKeepAlive = undefined;
}
if (this._fetchDataTimeout) {
clearInterval(this._fetchDataTimeout);
this._fetchDataTimeout = undefined;
}
}
protected render(): TemplateResult {
if (!this._addon) {
return html`<hass-loading-screen
.message=${this._loadingMessage}
></hass-loading-screen>`;
}
const iframe = html`<iframe
title=${this._addon.name}
src=${this._addon.ingress_url!}
@load=${this._checkLoaded}
>
</iframe>`;
if (!this.ingressPanel) {
return html`<hass-subpage
.hass=${this.hass}
.header=${this._addon.name}
.narrow=${this.narrow}
>
${iframe}
</hass-subpage>`;
}
return html`${this.narrow || this.hass.dockedSidebar === "always_hidden"
? html`<div class="header">
<ha-icon-button
.label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
.path=${mdiMenu}
@click=${this._toggleMenu}
></ha-icon-button>
<div class="main-title">${this._addon.name}</div>
</div>
${iframe}`
: iframe}`;
}
protected async firstUpdated(): Promise<void> {
if (this.route.path === "") {
const requestedAddon = extractSearchParam("addon");
let addonInfo: HassioAddonDetails;
if (requestedAddon) {
try {
addonInfo = await fetchHassioAddonInfo(this.hass, requestedAddon);
} catch (err: any) {
await showAlertDialog(this, {
text: extractApiErrorMessage(err),
title: requestedAddon,
});
await nextRender();
navigate("/hassio/store", { replace: true });
return;
}
if (!addonInfo.version) {
await showAlertDialog(this, {
text: this.supervisor.localize("my.error_app_not_installed"),
title: addonInfo.name,
});
await nextRender();
navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true });
} else if (!addonInfo.ingress) {
await showAlertDialog(this, {
text: this.supervisor.localize("my.error_app_no_ingress"),
title: addonInfo.name,
});
await nextRender();
navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true });
} else {
navigate(`/hassio/ingress/${addonInfo.slug}`, { replace: true });
}
}
}
}
protected willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!changedProps.has("route")) {
return;
}
const addon = this.route.path.substring(1);
const oldRoute = changedProps.get("route") as this["route"] | undefined;
const oldAddon = oldRoute ? oldRoute.path.substring(1) : undefined;
if (addon && addon !== oldAddon) {
this._loadingMessage = undefined;
this._fetchData(addon);
}
}
private async _fetchData(addonSlug: string) {
const createSessionPromise = createHassioSession(this.hass);
let addon: HassioAddonDetails;
try {
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
} catch (_err: any) {
await this.updateComplete;
await showAlertDialog(this, {
text:
this.supervisor.localize("ingress.error_app_info") ||
"Unable to fetch app info to start Ingress",
title: "Supervisor",
});
await nextRender();
navigate("/hassio/store", { replace: true });
return;
}
if (!addon.version) {
await this.updateComplete;
await showAlertDialog(this, {
text:
this.supervisor.localize("ingress.error_app_not_installed") ||
"The app is not installed. Please install it first",
title: addon.name,
});
await nextRender();
navigate(`/hassio/addon/${addon.slug}/info`, { replace: true });
return;
}
if (!addon.ingress_url) {
await this.updateComplete;
await showAlertDialog(this, {
text:
this.supervisor.localize("ingress.error_app_not_supported") ||
"This app does not support Ingress",
title: addon.name,
});
await nextRender();
goBack();
return;
}
if (!addon.state || !["startup", "started"].includes(addon.state)) {
await this.updateComplete;
const confirm = await showConfirmationDialog(this, {
text:
this.supervisor.localize("ingress.error_app_not_running") ||
"The app is not running. Do you want to start it now?",
title: addon.name,
confirmText:
this.supervisor.localize("ingress.start_app") || "Start app",
dismissText: this.supervisor.localize("common.no") || "No",
});
if (confirm) {
try {
this._loadingMessage =
this.supervisor.localize("ingress.app_starting") ||
"The app is starting, this can take some time...";
await startHassioAddon(this.hass, addonSlug);
fireEvent(this, "supervisor-collection-refresh", {
collection: "addon",
});
this._fetchData(addonSlug);
return;
} catch (_err) {
await showAlertDialog(this, {
text:
this.supervisor.localize("ingress.error_starting_app") ||
"Error starting the app",
title: addon.name,
});
await nextRender();
navigate(`/hassio/addon/${addon.slug}/logs`, { replace: true });
return;
}
} else {
await nextRender();
navigate(`/hassio/addon/${addon.slug}/info`, { replace: true });
return;
}
}
if (addon.state === "startup") {
// App is starting up, wait for it to start
this._loadingMessage =
this.supervisor.localize("ingress.app_starting") ||
"The app is starting, this can take some time...";
this._fetchDataTimeout = window.setTimeout(() => {
this._fetchData(addonSlug);
}, 500);
return;
}
if (addon.state !== "started") {
return;
}
this._loadingMessage = undefined;
if (this._fetchDataTimeout) {
clearInterval(this._fetchDataTimeout);
this._fetchDataTimeout = undefined;
}
let session: string;
try {
session = await createSessionPromise;
} catch (_err: any) {
if (this._sessionKeepAlive) {
clearInterval(this._sessionKeepAlive);
}
await showAlertDialog(this, {
text:
this.supervisor.localize("ingress.error_creating_session") ||
"Unable to create an Ingress session",
title: addon.name,
});
await nextRender();
goBack();
return;
}
if (this._sessionKeepAlive) {
clearInterval(this._sessionKeepAlive);
}
this._sessionKeepAlive = window.setInterval(async () => {
try {
await validateHassioSession(this.hass, session);
} catch (_err: any) {
session = await createHassioSession(this.hass);
}
}, 60000);
this._addon = addon;
}
private async _checkLoaded(ev): Promise<void> {
if (!this._addon) {
return;
}
if (ev.target.contentDocument.body.textContent === "502: Bad Gateway") {
await this.updateComplete;
showConfirmationDialog(this, {
text:
this.supervisor.localize("ingress.error_app_not_ready") ||
"The app seems to not be ready, it might still be starting. Do you want to try again?",
title: this._addon.name,
confirmText: this.supervisor.localize("ingress.retry") || "Retry",
dismissText: this.supervisor.localize("common.no") || "No",
confirm: async () => {
const addon = this._addon;
this._addon = undefined;
await Promise.all([
this.updateComplete,
new Promise((resolve) => {
setTimeout(resolve, 500);
}),
]);
this._addon = addon;
},
});
}
}
private _toggleMenu(): void {
fireEvent(this, "hass-toggle-menu");
}
static styles = css`
iframe {
display: block;
width: 100%;
height: 100%;
border: 0;
}
.header + iframe {
height: calc(100% - 40px);
}
.header {
display: flex;
align-items: center;
font-size: var(--ha-font-size-l);
height: 40px;
padding: 0 16px;
pointer-events: none;
background-color: var(--app-header-background-color);
font-weight: var(--ha-font-weight-normal);
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
box-sizing: border-box;
--mdc-icon-size: 20px;
}
.main-title {
margin: var(--margin-title);
line-height: var(--ha-line-height-condensed);
flex-grow: 1;
}
ha-icon-button {
pointer-events: auto;
}
hass-subpage {
--app-header-background-color: var(--sidebar-background-color);
--app-header-text-color: var(--sidebar-text-color);
--app-header-border-bottom: 1px solid var(--divider-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hassio-ingress-view": HassioIngressView;
}
}
-55
View File
@@ -1,55 +0,0 @@
import { css } from "lit";
export const hassioStyle = css`
.content {
margin: 8px;
}
h1,
.description,
.card-content {
color: var(--primary-text-color);
}
h1 {
font-size: 2em;
margin-bottom: 8px;
font-family: var(--ha-font-family-body);
-webkit-font-smoothing: var(--ha-font-smoothing);
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
font-size: var(--ha-font-size-2xl);
font-weight: var(--ha-font-weight-normal);
line-height: var(--ha-line-height-condensed);
padding-left: 8px;
padding-inline-start: 8px;
padding-inline-end: initial;
}
.description {
margin-top: 4px;
padding-left: 8px;
padding-inline-start: 8px;
padding-inline-end: initial;
}
.card-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
grid-gap: var(--ha-space-2);
}
@media screen and (min-width: 640px) {
.card-group {
grid-template-columns: repeat(auto-fit, minmax(300px, 0.5fr));
}
}
@media screen and (min-width: 1020px) {
.card-group {
grid-template-columns: repeat(auto-fit, minmax(300px, 0.333fr));
}
}
@media screen and (min-width: 1300px) {
.card-group {
grid-template-columns: repeat(auto-fit, minmax(300px, 0.25fr));
}
}
.error {
color: var(--error-color);
margin-top: 16px;
}
`;
-231
View File
@@ -1,231 +0,0 @@
import type { Collection, UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { LitElement } from "lit";
import { property, state } from "lit/decorators";
import { atLeastVersion } from "../../src/common/config/version";
import { computeLocalize } from "../../src/common/translations/localize";
import { fetchHassioAddonsInfo } from "../../src/data/hassio/addon";
import type { HassioResponse } from "../../src/data/hassio/common";
import {
fetchHassioHassOsInfo,
fetchHassioHostInfo,
} from "../../src/data/hassio/host";
import { fetchNetworkInfo } from "../../src/data/hassio/network";
import { fetchHassioResolution } from "../../src/data/hassio/resolution";
import {
fetchHassioHomeAssistantInfo,
fetchHassioInfo,
fetchHassioSupervisorInfo,
} from "../../src/data/hassio/supervisor";
import { fetchSupervisorStore } from "../../src/data/supervisor/store";
import type {
Supervisor,
SupervisorObject,
SupervisorKeys,
} from "../../src/data/supervisor/supervisor";
import {
getSupervisorEventCollection,
supervisorCollection,
cleanupSupervisorCollection,
} from "../../src/data/supervisor/supervisor";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
import type { HomeAssistant, Route } from "../../src/types";
import { getTranslation } from "../../src/util/common-translation";
import {
computeRTLDirection,
setDirectionStyles,
} from "../../src/common/util/compute_rtl";
declare global {
interface HASSDomEvents {
"supervisor-update": Partial<Supervisor>;
"supervisor-collection-refresh": { collection: SupervisorObject };
}
}
export class SupervisorBaseElement extends urlSyncMixin(
ProvideHassLitMixin(LitElement)
) {
@property({ attribute: false }) public route?: Route;
@property({ attribute: false }) public supervisor: Partial<Supervisor> = {
localize: () => "",
};
@state() private _unsubs: Record<string, UnsubscribeFunc> = {};
@state() private _collections: Record<string, Collection<unknown>> = {};
@state() private _language = "en";
public connectedCallback(): void {
super.connectedCallback();
if (!this.hasUpdated) {
return;
}
if (this.route?.prefix === "/hassio") {
this._initSupervisor();
}
}
public disconnectedCallback() {
super.disconnectedCallback();
Object.keys(this._unsubs).forEach((unsub) => {
this._unsubs[unsub]();
delete this._unsubs[unsub];
});
Object.keys(this._collections).forEach((collection) => {
cleanupSupervisorCollection(this.hass.connection, collection);
});
this._collections = {};
this.removeEventListener(
"supervisor-collection-refresh",
this._handleSupervisorStoreRefreshEvent
);
}
protected willUpdate(changedProperties: PropertyValues) {
if (!this.hasUpdated) {
if (this.route?.prefix === "/hassio") {
this._initSupervisor();
}
}
if (changedProperties.has("hass")) {
const oldHass = changedProperties.get("hass") as
| HomeAssistant
| undefined;
if (oldHass?.language !== this.hass.language) {
this._language = this.hass.language;
}
}
if (changedProperties.has("_language") || !this.hasUpdated) {
this._initializeLocalize();
this._applyDirection(this.hass);
}
}
protected _updateSupervisor(update: Partial<Supervisor>): void {
this.supervisor = { ...this.supervisor, ...update };
}
private async _initializeLocalize() {
const { language, data } = await getTranslation(null, this._language);
this._updateSupervisor({
localize: await computeLocalize<SupervisorKeys>(
this.constructor.prototype,
language,
{
[language]: data,
}
),
});
}
private async _handleSupervisorStoreRefreshEvent(ev) {
const collection = ev.detail.collection;
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
if (collection in this._collections) {
this._collections[collection].refresh();
}
return;
}
const response = await this.hass.callApi<HassioResponse<any>>(
"GET",
`hassio${supervisorCollection[collection]}`
);
this._updateSupervisor({ [collection]: response.data });
}
private _subscribeCollection(collection: string) {
if (this._unsubs[collection]) {
this._unsubs[collection]();
}
try {
this._unsubs[collection] = this._collections[collection].subscribe(
(data) =>
this._updateSupervisor({
[collection]: data,
})
);
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
}
}
private async _initSupervisor(): Promise<void> {
this.addEventListener(
"supervisor-collection-refresh",
this._handleSupervisorStoreRefreshEvent
);
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
Object.keys(supervisorCollection).forEach((collection) => {
if (collection in this._collections) {
this._subscribeCollection(collection);
this._collections[collection].refresh();
} else {
this._collections[collection] = getSupervisorEventCollection(
this.hass.connection,
collection,
supervisorCollection[collection]
);
if (this._collections[collection].state) {
// happens when the grace period of the collection unsubscribe has not passed yet
this._updateSupervisor({
[collection]: this._collections[collection].state,
});
}
this._subscribeCollection(collection);
}
});
} else {
const [
addon,
supervisor,
host,
core,
info,
os,
network,
resolution,
store,
] = await Promise.all([
fetchHassioAddonsInfo(this.hass),
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
fetchHassioHomeAssistantInfo(this.hass),
fetchHassioInfo(this.hass),
fetchHassioHassOsInfo(this.hass),
fetchNetworkInfo(this.hass),
fetchHassioResolution(this.hass),
fetchSupervisorStore(this.hass),
]);
this._updateSupervisor({
addon,
supervisor,
host,
core,
info,
os,
network,
resolution,
store,
});
this.addEventListener("supervisor-update", (ev) =>
this._updateSupervisor(ev.detail)
);
}
}
private _applyDirection(hass: HomeAssistant) {
const direction = computeRTLDirection(hass);
setDirectionStyles(direction, this);
}
}
-206
View File
@@ -1,206 +0,0 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-button";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
import type { HassioStats } from "../../../src/data/hassio/common";
import {
extractApiErrorMessage,
fetchHassioStats,
} from "../../../src/data/hassio/common";
import { restartCore } from "../../../src/data/supervisor/core";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import type { HomeAssistant } from "../../../src/types";
import { bytesToString } from "../../../src/util/bytes-to-string";
import "../components/supervisor-metric";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-core-info")
class HassioCoreInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@state() private _metrics?: HassioStats;
protected render(): TemplateResult | undefined {
const metrics = [
{
description: this.supervisor.localize("system.core.cpu_usage"),
value: this._metrics?.cpu_percent,
},
{
description: this.supervisor.localize("system.core.ram_usage"),
value: this._metrics?.memory_percent,
tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString(
this._metrics?.memory_limit
)}`,
},
];
return html`
<ha-card header="Core" outlined>
<div class="card-content">
<div>
<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("common.version")}
</span>
<span slot="description">
core-${this.supervisor.core.version}
</span>
</ha-settings-row>
<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("common.newest_version")}
</span>
<span slot="description">
core-${this.supervisor.core.version_latest}
</span>
${!atLeastVersion(this.hass.config.version, 2021, 12) &&
this.supervisor.core.update_available
? html`
<ha-button
appearance="plain"
href="/hassio/update-available/core"
>
${this.supervisor.localize("common.show")}
</ha-button>
`
: ""}
</ha-settings-row>
</div>
<div>
${metrics.map(
(metric) => html`
<supervisor-metric
.description=${metric.description}
.value=${metric.value ?? 0}
.tooltip=${metric.tooltip}
></supervisor-metric>
`
)}
</div>
</div>
<div class="card-actions">
<ha-progress-button
slot="primaryAction"
variant="danger"
@click=${this._coreRestart}
.title=${this.supervisor.localize("common.restart_name", {
name: "Core",
})}
>
${this.supervisor.localize("common.restart_name", { name: "Core" })}
</ha-progress-button>
</div>
</ha-card>
`;
}
protected firstUpdated(): void {
this._loadData();
}
private async _loadData(): Promise<void> {
this._metrics = await fetchHassioStats(this.hass, "core");
}
private async _coreRestart(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize("confirm.restart.title", {
name: "Home Assistant Core",
}),
text: this.supervisor.localize("confirm.restart.text", {
name: "Home Assistant Core",
}),
confirmText: this.supervisor.localize("common.restart"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await restartCore(this.hass);
} catch (err: any) {
if (this.hass.connection.connected) {
showAlertDialog(this, {
title: this.supervisor.localize("common.failed_to_restart_name", {
name: "Home Assistant Core",
}),
text: extractApiErrorMessage(err),
});
}
} finally {
button.progress = false;
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
ha-card {
height: 100%;
justify-content: space-between;
flex-direction: column;
display: flex;
}
.card-actions {
height: 48px;
border-top: none;
display: flex;
justify-content: flex-end;
align-items: center;
}
.card-content {
display: flex;
flex-direction: column;
height: calc(100% - 124px);
justify-content: space-between;
}
ha-settings-row {
padding: 0;
height: 54px;
width: 100%;
}
ha-settings-row[three-line] {
height: 74px;
}
ha-settings-row > span[slot="description"] {
white-space: normal;
color: var(--secondary-text-color);
}
ha-button-menu {
color: var(--secondary-text-color);
--mdc-menu-min-width: 200px;
}
a {
text-decoration: none;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-core-info": HassioCoreInfo;
}
}
-453
View File
@@ -1,453 +0,0 @@
import { mdiDotsVertical } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-button";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-list-item";
import "../../../src/components/ha-settings-row";
import {
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../src/data/hassio/common";
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
import {
changeHostOptions,
configSyncOS,
rebootHost,
shutdownHost,
} from "../../../src/data/hassio/host";
import type { NetworkInfo } from "../../../src/data/hassio/network";
import { fetchNetworkInfo } from "../../../src/data/hassio/network";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
showPromptDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import type { HomeAssistant } from "../../../src/types";
import {
getValueInPercentage,
roundWithOneDecimal,
} from "../../../src/util/calculate";
import "../components/supervisor-metric";
import { showHassioDatadiskDialog } from "../dialogs/datadisk/show-dialog-hassio-datadisk";
import { showHassioHardwareDialog } from "../dialogs/hardware/show-dialog-hassio-hardware";
import { showNetworkDialog } from "../dialogs/network/show-dialog-network";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-host-info")
class HassioHostInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
protected render(): TemplateResult | undefined {
const primaryIpAddress = this.supervisor.host.features.includes("network")
? this._primaryIpAddress(this.supervisor.network!)
: "";
const metrics = [
{
description: this.supervisor.localize("system.host.used_space"),
value: this._getUsedSpace(
this.supervisor.host.disk_used,
this.supervisor.host.disk_total
),
tooltip: `${this.supervisor.host.disk_used} GB/${this.supervisor.host.disk_total} GB`,
},
];
return html`
<ha-card header="Host" outlined>
<div class="card-content">
<div>
${this.supervisor.host.features.includes("hostname")
? html`<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("system.host.hostname")}
</span>
<span slot="description">
${this.supervisor.host.hostname}
</span>
<ha-button
@click=${this._changeHostnameClicked}
appearance="plain"
size="small"
>
${this.supervisor.localize("system.host.change")}
</ha-button>
</ha-settings-row>`
: ""}
${this.supervisor.host.features.includes("network")
? html`<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("system.host.ip_address")}
</span>
<span slot="description"> ${primaryIpAddress} </span>
<ha-button
@click=${this._changeNetworkClicked}
appearance="plain"
size="small"
>
${this.supervisor.localize("system.host.change")}
</ha-button>
</ha-settings-row>`
: ""}
<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("system.host.operating_system")}
</span>
<span slot="description">
${this.supervisor.host.operating_system}
</span>
${!atLeastVersion(this.hass.config.version, 2021, 12) &&
this.supervisor.os.update_available
? html`
<ha-button
appearance="plain"
size="small"
href="/hassio/update-available/os"
>
${this.supervisor.localize("common.show")}
</ha-button>
`
: ""}
</ha-settings-row>
${!this.supervisor.host.features.includes("haos")
? html`<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("system.host.docker_version")}
</span>
<span slot="description">
${this.supervisor.info.docker}
</span>
</ha-settings-row>`
: ""}
${this.supervisor.host.deployment
? html`<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("system.host.deployment")}
</span>
<span slot="description">
${this.supervisor.host.deployment}
</span>
</ha-settings-row>`
: ""}
</div>
<div>
${this.supervisor.host.disk_life_time !== null
? html` <ha-settings-row>
<span slot="heading">
${this.supervisor.localize("system.host.lifetime_used")}
</span>
<span slot="description">
${this.supervisor.host.disk_life_time} %
</span>
</ha-settings-row>`
: ""}
${metrics.map(
(metric) => html`
<supervisor-metric
.description=${metric.description}
.value=${metric.value ?? 0}
.tooltip=${metric.tooltip}
></supervisor-metric>
`
)}
</div>
</div>
<div class="card-actions">
${this.supervisor.host.features.includes("reboot")
? html`
<ha-progress-button variant="danger" @click=${this._hostReboot}>
${this.supervisor.localize("system.host.reboot_host")}
</ha-progress-button>
`
: ""}
${this.supervisor.host.features.includes("shutdown")
? html`
<ha-progress-button
variant="danger"
@click=${this._hostShutdown}
>
${this.supervisor.localize("system.host.shutdown_host")}
</ha-progress-button>
`
: ""}
<ha-button-menu>
<ha-icon-button
.label=${this.supervisor.localize("common.menu")}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<ha-list-item
.action=${"hardware"}
@click=${this._handleMenuAction}
>
${this.supervisor.localize("system.host.hardware")}
</ha-list-item>
${this.supervisor.host.features.includes("haos")
? html`
<ha-list-item
.action=${"import_from_usb"}
@click=${this._handleMenuAction}
>
${this.supervisor.localize("system.host.import_from_usb")}
</ha-list-item>
${this.supervisor.host.features.includes("os_agent") &&
atLeastVersion(this.supervisor.host.agent_version, 1, 2, 0)
? html`
<ha-list-item
.action=${"move_datadisk"}
@click=${this._handleMenuAction}
>
${this.supervisor.localize(
"system.host.move_datadisk"
)}
</ha-list-item>
`
: ""}
`
: ""}
</ha-button-menu>
</div>
</ha-card>
`;
}
protected firstUpdated(): void {
this._loadData();
}
private _getUsedSpace = memoizeOne((used: number, total: number) =>
roundWithOneDecimal(getValueInPercentage(used, 0, total))
);
private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => {
if (!network_info || !network_info.interfaces) {
return "";
}
return network_info.interfaces.find((a) => a.primary)?.ipv4?.address![0];
});
private async _handleMenuAction(ev) {
switch ((ev.target as any).action) {
case "hardware":
await this._showHardware();
break;
case "import_from_usb":
await this._importFromUSB();
break;
case "move_datadisk":
await this._moveDatadisk();
break;
}
}
private _moveDatadisk(): void {
showHassioDatadiskDialog(this, {
supervisor: this.supervisor,
});
}
private async _showHardware(): Promise<void> {
let hardware;
try {
hardware = await fetchHassioHardwareInfo(this.hass);
} catch (err: any) {
await showAlertDialog(this, {
title: this.supervisor.localize(
"system.host.failed_to_get_hardware_list"
),
text: extractApiErrorMessage(err),
});
return;
}
showHassioHardwareDialog(this, { supervisor: this.supervisor, hardware });
}
private async _hostReboot(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize("system.host.reboot_host"),
text: this.supervisor.localize("system.host.confirm_reboot"),
confirmText: this.supervisor.localize("system.host.reboot_host"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await rebootHost(this.hass);
} catch (err: any) {
// Ignore connection errors, these are all expected
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: this.supervisor.localize("system.host.failed_to_reboot"),
text: extractApiErrorMessage(err),
});
}
}
button.progress = false;
}
private async _hostShutdown(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize("system.host.shutdown_host"),
text: this.supervisor.localize("system.host.confirm_shutdown"),
confirmText: this.supervisor.localize("system.host.shutdown_host"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await shutdownHost(this.hass);
} catch (err: any) {
// Ignore connection errors, these are all expected
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: this.supervisor.localize("system.host.failed_to_shutdown"),
text: extractApiErrorMessage(err),
});
}
}
button.progress = false;
}
private async _changeNetworkClicked(): Promise<void> {
showNetworkDialog(this, {
supervisor: this.supervisor,
loadData: () => this._loadData(),
});
}
private async _changeHostnameClicked(): Promise<void> {
const curHostname: string = this.supervisor.host.hostname;
const hostname = await showPromptDialog(this, {
title: this.supervisor.localize("system.host.change_hostname"),
inputLabel: this.supervisor.localize("system.host.new_hostname"),
inputType: "string",
defaultValue: curHostname,
confirmText: this.supervisor.localize("common.update"),
});
if (hostname && hostname !== curHostname) {
try {
await changeHostOptions(this.hass, { hostname });
fireEvent(this, "supervisor-collection-refresh", {
collection: "host",
});
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize("system.host.failed_to_set_hostname"),
text: extractApiErrorMessage(err),
});
}
}
}
private async _importFromUSB(): Promise<void> {
try {
await configSyncOS(this.hass);
fireEvent(this, "supervisor-collection-refresh", {
collection: "host",
});
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize(
"system.host.failed_to_import_from_usb"
),
text: extractApiErrorMessage(err),
});
}
}
private async _loadData(): Promise<void> {
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
fireEvent(this, "supervisor-collection-refresh", {
collection: "network",
});
} else {
const network = await fetchNetworkInfo(this.hass);
fireEvent(this, "supervisor-update", { network });
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
ha-card {
height: 100%;
justify-content: space-between;
flex-direction: column;
display: flex;
}
.card-actions {
height: 48px;
border-top: none;
display: flex;
justify-content: space-between;
align-items: center;
}
.card-content {
display: flex;
flex-direction: column;
height: calc(100% - 124px);
justify-content: space-between;
}
ha-settings-row {
padding: 0;
height: 54px;
width: 100%;
}
ha-settings-row[three-line] {
height: 74px;
}
ha-settings-row > span[slot="description"] {
white-space: normal;
color: var(--secondary-text-color);
}
ha-button-menu {
color: var(--secondary-text-color);
--mdc-menu-min-width: 200px;
}
ha-list-item ha-svg-icon {
color: var(--secondary-text-color);
}
a {
text-decoration: none;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-host-info": HassioHostInfo;
}
}
-469
View File
@@ -1,469 +0,0 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-alert";
import "../../../src/components/ha-button";
import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-switch";
import type { HassioStats } from "../../../src/data/hassio/common";
import {
extractApiErrorMessage,
fetchHassioStats,
} from "../../../src/data/hassio/common";
import type { SupervisorOptions } from "../../../src/data/hassio/supervisor";
import {
reloadSupervisor,
restartSupervisor,
setSupervisorOption,
} from "../../../src/data/hassio/supervisor";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { showJoinBetaDialog } from "../../../src/panels/config/core/updates/show-dialog-join-beta";
import {
UNHEALTHY_REASON_URL,
UNSUPPORTED_REASON_URL,
} from "../../../src/panels/config/repairs/dialog-system-information";
import { haStyle } from "../../../src/resources/styles";
import type { HomeAssistant } from "../../../src/types";
import { bytesToString } from "../../../src/util/bytes-to-string";
import { documentationUrl } from "../../../src/util/documentation-url";
import "../components/supervisor-metric";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-supervisor-info")
class HassioSupervisorInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@state() private _metrics?: HassioStats;
protected render(): TemplateResult | undefined {
const metrics = [
{
description: this.supervisor.localize("system.supervisor.cpu_usage"),
value: this._metrics?.cpu_percent,
},
{
description: this.supervisor.localize("system.supervisor.ram_usage"),
value: this._metrics?.memory_percent,
tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString(
this._metrics?.memory_limit
)}`,
},
];
return html`
<ha-card header="Supervisor" outlined>
<div class="card-content">
<div>
<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("common.version")}
</span>
<span slot="description">
supervisor-${this.supervisor.supervisor.version}
</span>
</ha-settings-row>
<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("common.newest_version")}
</span>
<span slot="description">
supervisor-${this.supervisor.supervisor.version_latest}
</span>
${!atLeastVersion(this.hass.config.version, 2021, 12) &&
this.supervisor.supervisor.update_available
? html`
<ha-button
appearance="plain"
size="small"
href="/hassio/update-available/supervisor"
>
${this.supervisor.localize("common.show")}
</ha-button>
`
: ""}
</ha-settings-row>
<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("system.supervisor.channel")}
</span>
<span slot="description">
${this.supervisor.supervisor.channel}
</span>
${this.supervisor.supervisor.channel === "beta"
? html`
<ha-progress-button
@click=${this._toggleBeta}
.title=${this.supervisor.localize(
"system.supervisor.leave_beta_description"
)}
>
${this.supervisor.localize(
"system.supervisor.leave_beta_action"
)}
</ha-progress-button>
`
: this.supervisor.supervisor.channel === "stable"
? html`
<ha-progress-button
@click=${this._toggleBeta}
.title=${this.supervisor.localize(
"system.supervisor.join_beta_description"
)}
>
${this.supervisor.localize(
"system.supervisor.join_beta_action"
)}
</ha-progress-button>
`
: ""}
</ha-settings-row>
${this.supervisor.supervisor.supported
? !atLeastVersion(this.hass.config.version, 2021, 4)
? html` <ha-settings-row three-line>
<span slot="heading">
${this.supervisor.localize(
"system.supervisor.share_diagnostics"
)}
</span>
<div slot="description" class="diagnostics-description">
${this.supervisor.localize(
"system.supervisor.share_diagnostics_description"
)}
<button
class="link"
.title=${this.supervisor.localize("common.show_more")}
@click=${this._diagnosticsInformationDialog}
>
${this.supervisor.localize("common.learn_more")}
</button>
</div>
<ha-switch
haptic
.checked=${this.supervisor.supervisor.diagnostics}
@change=${this._toggleDiagnostics}
></ha-switch>
</ha-settings-row>`
: ""
: html`<ha-alert alert-type="warning">
${this.supervisor.localize(
"system.supervisor.unsupported_title"
)}
<ha-button
slot="action"
@click=${this._unsupportedDialog}
variant="warning"
size="small"
>
${this.supervisor.localize("common.learn_more")}
</ha-button>
</ha-alert>`}
${!this.supervisor.supervisor.healthy
? html`<ha-alert alert-type="error">
${this.supervisor.localize(
"system.supervisor.unhealthy_title"
)}
<ha-button
variant="danger"
size="small"
slot="action"
@click=${this._unhealthyDialog}
>
${this.supervisor.localize("common.learn_more")}
</ha-button>
</ha-alert>`
: ""}
</div>
<div class="metrics-block">
${metrics.map(
(metric) => html`
<supervisor-metric
.description=${metric.description}
.value=${metric.value ?? 0}
.tooltip=${metric.tooltip}
></supervisor-metric>
`
)}
</div>
</div>
<div class="card-actions">
<ha-progress-button
@click=${this._supervisorReload}
.title=${this.supervisor.localize(
"system.supervisor.reload_supervisor"
)}
>
${this.supervisor.localize("system.supervisor.reload_supervisor")}
</ha-progress-button>
<ha-progress-button
class="warning"
@click=${this._supervisorRestart}
.title=${this.supervisor.localize("common.restart_name", {
name: "Supervisor",
})}
>
${this.supervisor.localize("common.restart_name", {
name: "Supervisor",
})}
</ha-progress-button>
</div>
</ha-card>
`;
}
protected firstUpdated(): void {
this._loadData();
}
private async _loadData(): Promise<void> {
this._metrics = await fetchHassioStats(this.hass, "supervisor");
}
private async _toggleBeta(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
if (this.supervisor.supervisor.channel === "stable") {
showJoinBetaDialog(this, {
join: async () => {
await this._setChannel("beta");
button.progress = false;
},
cancel: () => {
button.progress = false;
},
});
} else {
await this._setChannel("stable");
button.progress = false;
}
}
private async _setChannel(
channel: SupervisorOptions["channel"]
): Promise<void> {
try {
const data: Partial<SupervisorOptions> = {
channel,
};
await setSupervisorOption(this.hass, data);
await this._reloadSupervisor();
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize(
"system.supervisor.failed_to_set_option"
),
text: extractApiErrorMessage(err),
});
}
}
private async _supervisorReload(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
try {
await this._reloadSupervisor();
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize("system.supervisor.failed_to_reload"),
text: extractApiErrorMessage(err),
});
} finally {
button.progress = false;
}
}
private async _reloadSupervisor(): Promise<void> {
await reloadSupervisor(this.hass);
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",
});
}
private async _supervisorRestart(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize("confirm.restart.title", {
name: "Supervisor",
}),
text: this.supervisor.localize("confirm.restart.text", {
name: "Supervisor",
}),
confirmText: this.supervisor.localize("common.restart"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await restartSupervisor(this.hass);
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize("common.failed_to_restart_name", {
name: "Supervisor",
}),
text: extractApiErrorMessage(err),
});
} finally {
button.progress = false;
}
}
private async _diagnosticsInformationDialog(): Promise<void> {
await showAlertDialog(this, {
title: this.supervisor.localize(
"system.supervisor.share_diagonstics_title"
),
text: this.supervisor.localize(
"system.supervisor.share_diagonstics_description",
{ line_break: html`<br /><br />` }
),
});
}
private async _unsupportedDialog(): Promise<void> {
await showAlertDialog(this, {
title: this.supervisor.localize("system.supervisor.unsupported_title"),
text: html`${this.supervisor.localize(
"system.supervisor.unsupported_description"
)} <br /><br />
<ul>
${this.supervisor.resolution.unsupported.map(
(reason) => html`
<li>
<a
href=${documentationUrl(
this.hass,
UNSUPPORTED_REASON_URL[reason] ||
`/more-info/unsupported/${reason}`
)}
target="_blank"
rel="noreferrer"
>
${this.supervisor.localize(
`system.supervisor.unsupported_reason.${reason}`
) || reason}
</a>
</li>
`
)}
</ul>`,
});
}
private async _unhealthyDialog(): Promise<void> {
await showAlertDialog(this, {
title: this.supervisor.localize("system.supervisor.unhealthy_title"),
text: html`${this.supervisor.localize(
"system.supervisor.unhealthy_description"
)} <br /><br />
<ul>
${this.supervisor.resolution.unhealthy.map(
(reason) => html`
<li>
<a
href=${documentationUrl(
this.hass,
UNHEALTHY_REASON_URL[reason] ||
`/more-info/unhealthy/${reason}`
)}
target="_blank"
rel="noreferrer"
>
${this.supervisor.localize(
`system.supervisor.unhealthy_reason.${reason}`
) || reason}
</a>
</li>
`
)}
</ul>`,
});
}
private async _toggleDiagnostics(): Promise<void> {
try {
const data: SupervisorOptions = {
diagnostics: !this.supervisor.supervisor?.diagnostics,
};
await setSupervisorOption(this.hass, data);
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize(
"system.supervisor.failed_to_set_option"
),
text: extractApiErrorMessage(err),
});
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
ha-card {
height: 100%;
justify-content: space-between;
flex-direction: column;
display: flex;
}
.card-actions {
height: 48px;
border-top: none;
display: flex;
justify-content: space-between;
align-items: center;
}
.card-content {
display: flex;
flex-direction: column;
height: calc(100% - 124px);
justify-content: space-between;
}
.metrics-block {
margin-top: 16px;
}
button.link {
color: var(--primary-color);
}
ha-settings-row {
padding: 0;
height: 54px;
width: 100%;
}
ha-settings-row[three-line] {
height: 74px;
}
ha-settings-row > div[slot="description"] {
white-space: normal;
color: var(--secondary-text-color);
}
a {
text-decoration: none;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-supervisor-info": HassioSupervisorInfo;
}
}
-162
View File
@@ -1,162 +0,0 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-alert";
import "../../../src/components/ha-ansi-to-html";
import "../../../src/components/ha-card";
import "../../../src/components/ha-select";
import "../../../src/components/ha-list-item";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import "../../../src/layouts/hass-loading-screen";
import { haStyle } from "../../../src/resources/styles";
import type { HomeAssistant } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style";
interface LogProvider {
key: string;
name: string;
}
const logProviders: LogProvider[] = [
{
key: "supervisor",
name: "Supervisor",
},
{
key: "core",
name: "Core",
},
{
key: "host",
name: "Host",
},
{
key: "dns",
name: "DNS",
},
{
key: "audio",
name: "Audio",
},
{
key: "multicast",
name: "Multicast",
},
];
@customElement("hassio-supervisor-log")
class HassioSupervisorLog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@state() private _error?: string;
@state() private _selectedLogProvider = "supervisor";
@state() private _content?: string;
public async connectedCallback(): Promise<void> {
super.connectedCallback();
await this._loadData();
}
protected render(): TemplateResult | undefined {
return html`
<ha-card outlined>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${this.hass.userData?.showAdvanced
? html`
<ha-select
.label=${this.supervisor.localize("system.log.log_provider")}
@selected=${this._setLogProvider}
.value=${this._selectedLogProvider}
>
${logProviders.map(
(provider) => html`
<ha-list-item .value=${provider.key}>
${provider.name}
</ha-list-item>
`
)}
</ha-select>
`
: ""}
<div class="card-content" id="content">
${this._content
? html`<ha-ansi-to-html .content=${this._content}>
</ha-ansi-to-html>`
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
</div>
<div class="card-actions">
<ha-progress-button @click=${this._refresh}>
${this.supervisor.localize("common.refresh")}
</ha-progress-button>
</div>
</ha-card>
`;
}
private async _setLogProvider(ev): Promise<void> {
const provider = ev.target.value;
this._selectedLogProvider = provider;
this._loadData();
}
private async _refresh(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
await this._loadData();
button.progress = false;
}
private async _loadData(): Promise<void> {
this._error = undefined;
try {
const response = await fetchHassioLogs(
this.hass,
this._selectedLogProvider
);
this._content = await response.text();
} catch (err: any) {
this._error = this.supervisor.localize("system.log.get_logs", {
provider: this._selectedLogProvider,
error: extractApiErrorMessage(err),
});
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
ha-card {
margin-top: 8px;
width: 100%;
}
pre {
white-space: pre-wrap;
}
ha-select {
width: 100%;
margin-bottom: 4px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-supervisor-log": HassioSupervisorLog;
}
}
-93
View File
@@ -1,93 +0,0 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles";
import type { HomeAssistant, Route } from "../../../src/types";
import { supervisorTabs } from "../hassio-tabs";
import { hassioStyle } from "../resources/hassio-style";
import "./hassio-core-info";
import "./hassio-host-info";
import "./hassio-supervisor-info";
import "./hassio-supervisor-log";
@customElement("hassio-system")
class HassioSystem extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
protected render(): TemplateResult | undefined {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${supervisorTabs(this.hass)}
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
back-path="/config"
supervisor
>
<span slot="header"> ${this.supervisor.localize("panel.system")} </span>
<div class="content">
<div class="card-group">
<hassio-core-info
.hass=${this.hass}
.supervisor=${this.supervisor}
></hassio-core-info>
<hassio-supervisor-info
.hass=${this.hass}
.supervisor=${this.supervisor}
></hassio-supervisor-info>
<hassio-host-info
.hass=${this.hass}
.supervisor=${this.supervisor}
></hassio-host-info>
</div>
<hassio-supervisor-log
.hass=${this.hass}
.supervisor=${this.supervisor}
></hassio-supervisor-log>
</div>
</hass-tabs-subpage>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
.content {
margin: 8px;
color: var(--primary-text-color);
}
.title {
margin-top: 24px;
color: var(--primary-text-color);
font-size: 2em;
padding-left: 8px;
padding-inline-start: 8px;
padding-inline-end: initial;
margin-bottom: 8px;
}
hassio-supervisor-log {
width: 100%;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-system": HassioSystem;
}
}
@@ -1,507 +0,0 @@
import {
css,
type CSSResultGroup,
html,
LitElement,
nothing,
type PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-alert";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
import "../../../src/components/ha-spinner";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-faded";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-markdown";
import "../../../src/components/ha-md-list";
import "../../../src/components/ha-md-list-item";
import "../../../src/components/ha-svg-icon";
import "../../../src/components/ha-switch";
import type { HaSwitch } from "../../../src/components/ha-switch";
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
import {
fetchHassioAddonChangelog,
fetchHassioAddonInfo,
updateHassioAddon,
} from "../../../src/data/hassio/addon";
import {
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../src/data/hassio/common";
import { fetchHassioHassOsInfo, updateOS } from "../../../src/data/hassio/host";
import {
fetchHassioHomeAssistantInfo,
fetchHassioSupervisorInfo,
updateSupervisor,
} from "../../../src/data/hassio/supervisor";
import { updateCore } from "../../../src/data/supervisor/core";
import type { StoreAddon } from "../../../src/data/supervisor/store";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import type { HomeAssistant, Route } from "../../../src/types";
import { addonArchIsSupported, extractChangelog } from "../util/addon";
declare global {
interface HASSDomEvents {
"update-complete": undefined;
}
}
const SUPERVISOR_UPDATE_NAMES = {
core: "Home Assistant Core",
os: "Home Assistant Operating System",
supervisor: "Home Assistant Supervisor",
};
type UpdateType = "os" | "supervisor" | "core" | "addon";
const changelogUrl = (
entry: UpdateType,
version: string
): string | undefined => {
if (entry === "addon") {
return undefined;
}
if (entry === "core") {
return version.includes("dev")
? "https://github.com/home-assistant/core/commits/dev"
: version.includes("b")
? "https://next.home-assistant.io/latest-release-notes/"
: "https://www.home-assistant.io/latest-release-notes/";
}
if (entry === "os") {
return version.includes("dev")
? "https://github.com/home-assistant/operating-system/commits/dev"
: `https://github.com/home-assistant/operating-system/releases/tag/${version}`;
}
if (entry === "supervisor") {
return version.includes("dev")
? "https://github.com/home-assistant/supervisor/commits/main"
: `https://github.com/home-assistant/supervisor/releases/tag/${version}`;
}
return undefined;
};
@customElement("update-available-card")
class UpdateAvailableCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public addonSlug?: string;
@state() private _updateType?: UpdateType;
@state() private _changelogContent?: string;
@state() private _addonInfo?: HassioAddonDetails;
@state() private _updating = false;
@state() private _error?: string;
private _addonStoreInfo = memoizeOne(
(slug: string, storeAddons: StoreAddon[]) =>
storeAddons.find((addon) => addon.slug === slug)
);
protected render() {
if (
!this._updateType ||
(this._updateType === "addon" && !this._addonInfo)
) {
return nothing;
}
const changelog = changelogUrl(this._updateType, this._version_latest);
const createBackupTexts = this._computeCreateBackupTexts();
return html`
<ha-card
outlined
.header=${this.supervisor.localize("update_available.update_name", {
name: this._name,
})}
>
<div class="card-content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${this._version === this._version_latest
? html`<p>
${this.supervisor.localize("update_available.no_update", {
name: this._name,
})}
</p>`
: !this._updating
? html`
${this._changelogContent
? html`
<ha-faded>
<ha-markdown .content=${this._changelogContent}>
</ha-markdown>
</ha-faded>
`
: nothing}
<div class="versions">
<p>
${this.supervisor.localize(
"update_available.description",
{
name: this._name,
version: this._version,
newest_version: this._version_latest,
}
)}
</p>
</div>
${createBackupTexts
? html`
<hr />
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${createBackupTexts.title}
</span>
${createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing}
<ha-switch
slot="end"
id="create-backup"
></ha-switch>
</ha-md-list-item>
</ha-md-list>
`
: nothing}
`
: html`<ha-spinner
aria-label="Updating"
size="large"
></ha-spinner>
<p class="progress-text">
${this.supervisor.localize("update_available.updating", {
name: this._name,
version: this._version_latest,
})}
</p>`}
</div>
${this._version !== this._version_latest && !this._updating
? html`
<div class="card-actions">
${changelog
? html`
<ha-button
href=${changelog}
target="_blank"
rel="noreferrer"
appearance="plain"
>
${this.supervisor.localize(
"update_available.open_release_notes"
)}
</ha-button>
`
: nothing}
<span></span>
<ha-progress-button @click=${this._update}>
${this.supervisor.localize("common.update")}
</ha-progress-button>
</div>
`
: nothing}
</ha-card>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
const pathPart = this.route?.path.substring(1, this.route.path.length);
const updateType = ["core", "os", "supervisor"].includes(pathPart)
? pathPart
: "addon";
this._updateType = updateType as UpdateType;
switch (updateType) {
case "addon":
if (!this.addonSlug) {
this.addonSlug = pathPart;
}
this._loadAddonData();
break;
case "core":
this._loadCoreData();
break;
case "supervisor":
this._loadSupervisorData();
break;
case "os":
this._loadOsData();
break;
}
}
private _computeCreateBackupTexts():
| { title: string; description?: string }
| undefined {
// App backup
if (
this._updateType === "addon" &&
atLeastVersion(this.hass.config.version, 2025, 2, 0)
) {
const version = this._version;
return {
title: this.supervisor.localize("update_available.create_backup.app"),
description: this.supervisor.localize(
"update_available.create_backup.app_description",
{ version: version }
),
};
}
// Old behavior
if (this._updateType && ["core", "addon"].includes(this._updateType)) {
return {
title: this.supervisor.localize(
"update_available.create_backup.generic"
),
};
}
return undefined;
}
get _shouldCreateBackup(): boolean {
if (this._updateType && !["core", "addon"].includes(this._updateType)) {
return false;
}
const createBackupSwitch = this.shadowRoot?.getElementById(
"create-backup"
) as HaSwitch;
if (createBackupSwitch) {
return createBackupSwitch.checked;
}
return true;
}
get _version(): string {
return this._updateType
? this._updateType === "addon"
? this._addonInfo!.version
: this.supervisor[this._updateType]?.version || ""
: "";
}
get _version_latest(): string {
return this._updateType
? this._updateType === "addon"
? this._addonInfo!.version_latest
: this.supervisor[this._updateType]?.version_latest || ""
: "";
}
get _name(): string {
return this._updateType
? this._updateType === "addon"
? this._addonInfo!.name
: SUPERVISOR_UPDATE_NAMES[this._updateType]
: "";
}
private async _loadAddonData() {
try {
this._addonInfo = await fetchHassioAddonInfo(this.hass, this.addonSlug!);
} catch (err) {
showAlertDialog(this, {
title: this._updateType,
text: extractApiErrorMessage(err),
});
return;
}
const addonStoreInfo =
!this._addonInfo.detached && !this._addonInfo.available
? this._addonStoreInfo(
this._addonInfo.slug,
this.supervisor.store.addons
)
: undefined;
if (this._addonInfo.changelog) {
try {
const content = await fetchHassioAddonChangelog(
this.hass,
this.addonSlug!
);
this._changelogContent = extractChangelog(this._addonInfo, content);
} catch (err) {
this._error = extractApiErrorMessage(err);
return;
}
}
if (!this._addonInfo.available && addonStoreInfo) {
if (
!addonArchIsSupported(
this.supervisor.info.supported_arch,
this._addonInfo.arch
)
) {
this._error = this.supervisor.localize(
"app.dashboard.not_available_arch"
);
} else {
this._error = this.supervisor.localize(
"app.dashboard.not_available_version",
{
core_version_installed: this.supervisor.core.version,
core_version_needed: addonStoreInfo.homeassistant,
}
);
}
}
}
private async _loadSupervisorData() {
try {
const supervisor = await fetchHassioSupervisorInfo(this.hass);
fireEvent(this, "supervisor-update", { supervisor });
} catch (err) {
showAlertDialog(this, {
title: this._updateType,
text: extractApiErrorMessage(err),
});
}
}
private async _loadCoreData() {
try {
const core = await fetchHassioHomeAssistantInfo(this.hass);
fireEvent(this, "supervisor-update", { core });
} catch (err) {
showAlertDialog(this, {
title: this._updateType,
text: extractApiErrorMessage(err),
});
}
}
private async _loadOsData() {
try {
const os = await fetchHassioHassOsInfo(this.hass);
fireEvent(this, "supervisor-update", { os });
} catch (err) {
showAlertDialog(this, {
title: this._updateType,
text: extractApiErrorMessage(err),
});
}
}
private async _update() {
if (this._shouldCreateBackup && this.supervisor.info.state === "freeze") {
this._error = this.supervisor.localize("backup.backup_already_running");
return;
}
this._error = undefined;
this._updating = true;
try {
if (this._updateType === "addon") {
await updateHassioAddon(
this.hass,
this.addonSlug!,
this._shouldCreateBackup
);
} else if (this._updateType === "core") {
await updateCore(this.hass, this._shouldCreateBackup);
} else if (this._updateType === "os") {
await updateOS(this.hass);
} else if (this._updateType === "supervisor") {
await updateSupervisor(this.hass);
}
} catch (err: any) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err);
this._updating = false;
return;
}
}
fireEvent(this, "update-complete");
this._updating = false;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
display: block;
}
ha-card {
margin: auto;
}
a {
text-decoration: none;
color: var(--primary-text-color);
}
.card-actions {
display: flex;
justify-content: space-between;
}
ha-spinner {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
ha-markdown {
padding-bottom: 8px;
}
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: 16px 0 0 0;
}
ha-md-list {
padding: 0;
margin-bottom: -16px;
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-item-overflow: visible;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"update-available-card": UpdateAvailableCard;
}
}

Some files were not shown because too many files have changed in this diff Show More