Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbb5cca794 | ||
|
|
6ddbb68444 | ||
|
|
906cd7bbec | ||
|
|
30467c3a51 | ||
|
|
fc760b9325 |
No files matched your search
@@ -38,7 +38,6 @@ Container components may keep `hass` when they own it and feed providers. Leaf c
|
||||
| `uiContext` | themes, selected theme, panels, sidebar, and UI state |
|
||||
| `narrowViewportContext` | narrow-layout boolean |
|
||||
| `reducedMotionContext` | `prefers-reduced-motion` media query |
|
||||
| `brandsAccessTokenContext` | the `hass` rerender that brand images wait for to get their token |
|
||||
|
||||
Lazy contexts subscribe on first consumer and tear down after the last consumer: `labelsContext`, `fullEntitiesContext`, `configEntriesContext`, `manifestsContext`, `triggerDescriptionsContext`, and `conditionDescriptionsContext`.
|
||||
|
||||
@@ -94,6 +93,5 @@ To consume a whole group untransformed, omit `@transform` and type the field as
|
||||
- The component consumes the narrowest context needed for the data it reads.
|
||||
- A broad `hass` property is kept only when the component is a container or external API requires it.
|
||||
- Entity-scoped reads use the consume helpers rather than ad hoc context transforms.
|
||||
- A component that renders brand images consumes `brandsAccessTokenContext` with `@state()`, so the images fill in once the token arrives or rotates.
|
||||
- Context fields read during render or `willUpdate` are marked `@state()` so updates trigger rendering. Fields only read outside the render cycle are left without `@state()`.
|
||||
- Tests and mocks only provide the data the component actually consumes.
|
||||
@@ -1,91 +0,0 @@
|
||||
---
|
||||
name: ha-frontend-query-params
|
||||
description: Home Assistant frontend URL query param patterns. Use when keeping page state such as filters, search, sorting or grouping in the URL, adding or reading typed query params, using QueryParamsController or the data table's sync-url and column urlName, building in-app links with a query string, or adding params to My Home Assistant redirects.
|
||||
---
|
||||
|
||||
# HA Frontend Query Params
|
||||
|
||||
Use this skill when implementing or reviewing state kept in the URL's query string, or links that set it. Cross-load `ha-frontend-lit` for the controller and lifecycle work, `ha-frontend-testing` for running tests, and `ha-frontend-review` when reviewing.
|
||||
|
||||
## Typed Params
|
||||
|
||||
`src/common/url/query-params.ts` turns a typed config into values and back:
|
||||
|
||||
- `QueryParamConfig` lists keys by type: `string`, `list` (one comma-separated param, `?label=a,b`), `date` (ISO string) and `boolean` (`{ key, trueValue }`, true only when the param equals `trueValue`).
|
||||
- `QueryParamValues<typeof config>` is the matching partial values type.
|
||||
- `decodeQueryParams(source, config)` reads a `SearchParamsSource` (a `URLSearchParams`, a plain record or a query string). Empty values are skipped.
|
||||
- `createQueryString(values, config)` writes the values, leaving out empty strings, empty lists and false booleans.
|
||||
- `updateQueryString(current, values, config)` replaces only the config's params in an existing query string and keeps every other param exactly as written.
|
||||
- `hasQueryParams(source, config)` tells whether a URL has any of the config's params, which is how a page knows whether a link asked for anything.
|
||||
- `serviceTargetFromQueryParams` and `queryParamsFromServiceTarget` map `HassServiceTarget` keys to list params.
|
||||
|
||||
Give each feature its own `*-query-params.ts` in `src/common/url/` holding its config (`as const satisfies QueryParamConfig`), its values type, and the helpers that turn params into page state and back. Follow the existing ones: `data-table-query-params.ts`, `marketplace-query-params.ts`, `history-logbook-query-params.ts` and `todo-query-params.ts`. Helpers in `src/data/` that build outside links use a local config the same way, as `src/data/integration.ts` and `src/data/system_log_report.ts` do. In new code, prefer these over hand-built `URLSearchParams`, string concatenation or `extractSearchParam` reads from `src/common/url/search-params.ts`.
|
||||
|
||||
## QueryParamsController
|
||||
|
||||
`src/common/controllers/query-params-controller.ts` keeps part of a page's state in the URL. Construct it with the host, the feature's config and options:
|
||||
|
||||
- `enabled()`: whether the host syncs right now. It can change, as on the Marketplace dashboard, which turns it off on the Discover tab.
|
||||
- `navigated()`: called when the page's URL changes while the host is connected (`location-changed` or `popstate`), and when the host reconnects after its first update. Read the URL with `searchParams()`, apply it, then `write()` the state back.
|
||||
|
||||
Methods:
|
||||
|
||||
- `searchParams()` returns the current params, or `undefined` when the controller is disabled or the URL is on another path.
|
||||
- `write(values, debounced?)` updates only the config's params with `replaceCurrentUrl`, so it replaces the current history entry. Pass `debounced` for values that change while typing; the write lands 300 ms after the last one and any later write replaces it.
|
||||
- `flush()` writes a pending debounced write now. The data table calls it when the search box loses focus. Call it before anything that adds its own history entry, such as a dialog.
|
||||
|
||||
The controller binds to `currentPath()` when the host connects and does nothing while the URL is on another path. Navigating away drops a pending write. When a page is reached through a router `defaultPage` (such as `/marketplace` landing on `/marketplace/browse`), check that a link to the bare path still syncs, since the redirect may land after the host connects.
|
||||
|
||||
Read the URL yourself on the first render, after the initial state is set (in `willUpdate` before `hasUpdated`, or `firstUpdated`), and leave later navigations to `navigated()`. `ha-marketplace-dashboard.ts` and `lorawan-devices.ts` show both. When moving a page to the controller, remove its own `location-changed` and `popstate` listeners and any search kept in history state, since the controller replaces them.
|
||||
|
||||
When a router reuses one element for different items, such as one app's tab after another's, key it on the item so the controller binds again to the new path.
|
||||
|
||||
## URL Rules
|
||||
|
||||
- Writes replace the current history entry, so Back leaves the page instead of undoing each change. Push a new entry only for a deliberate navigation, such as picking another log provider.
|
||||
- Debounce search writes. Write sort, group and filter changes straight away.
|
||||
- Leave empty and default values out of the URL, such as an empty search or ascending direction.
|
||||
- A param in the URL wins over stored preferences. A link with none of the page's params keeps what the user picked before; `hasQueryParams` makes that check.
|
||||
- A link with any of the page's params replaces the search and every filter, since a filter left over from before would hide part of what the link shows. Show what the link applied, but don't store it: links often carry someone else's choices, such as a My Home Assistant link made with `m`. Store only what the user changes on the page, and only that setting.
|
||||
- The URL is the whole state. Don't add filters while applying a link; a link that needs one, such as showing disabled devices, says so in its params (`status=disabled`).
|
||||
- A link to a page that is already open must apply the new URL. Handle `navigated()`, not only the first render. The router often keeps a page alive, so this is the common case, not an edge case.
|
||||
- Use one comma-separated param for several values (`?status=new,installed`), which `list` params do.
|
||||
- Use the names other pages already use for the same thing: `search`, `sort`, `direction` and `group` for the table, and `floor`, `area`, `device`, `label`, `config_entry`, `sub_entry` and `assistants` for the shared filters.
|
||||
- A search box's text is always `search`, never `filter` or `q`.
|
||||
- Keep existing param names so existing links and My Home Assistant links keep working. When renaming, read the old name as well for about six months, with a code comment naming the release it can be removed in, and write only the new name. Keep the old name in the config, use it only as a fallback when reading, and leave it out of the values you write, so the next write removes it from the URL.
|
||||
- Pages opt in. Some use params for other things, such as `?domain=` starting the add integration flow on the integrations dashboard, so check what a page already reads before syncing it.
|
||||
- Collapsed groups, column order and hidden columns stay in storage (`@storage`), not the URL.
|
||||
- Link to a page by its main path, not an alias, so a link to an open page lands on the same path and applies.
|
||||
|
||||
## Data Tables
|
||||
|
||||
`hass-tabs-subpage-data-table` with the `sync-url` attribute keeps `search`, `sort`, `direction` and `group` in the URL through `dataTableQueryParamsFromState` and `dataTableQueryStateFromUrl`. A linked search replaces the current one, and sorting or grouping a link leaves out keeps what was picked before. The table fires `search-changed`, `sorting-changed` and `grouping-changed` when a link changes them, with `link` set, so the page shows them without storing them. Use `DataTableSettingsController` (`src/common/controllers/data-table-settings-controller.ts`) for the stored search, sorting and grouping: give it the page's storage keys, bind its `search`, `sorting` and `grouping` to the table, and its `searchChanged`, `sortingChanged` and `groupingChanged` to the table's events. Call `showSearch` for a search the page reads from a link itself. A page the router keeps open shows what the user stored when it's opened again without params, like from the sidebar: the controller and table restore the search, sorting and grouping, and a page with filters restores its stored filters when its URL has no params.
|
||||
|
||||
- Give sortable and groupable columns a short public `urlName` (for example `type`, not `translated_category`). Sorting falls back to the column id without one; grouping is only written for columns that have one, so internal ids don't end up in links.
|
||||
- When a column definition is shared between pages, set its `urlName` where the shared column is defined, so every page uses the same name.
|
||||
- The table reads the URL once, as soon as it has columns. When a column only exists once data has loaded, such as Filename on the dashboards page, turn syncing on only after loading succeeds (`?sync-url=${!this._loading && !this._loadFailed}`), or a link that sorts or groups by that column is dropped.
|
||||
- The page owns its filters. Give it a filter config in its `*-query-params.ts`, a helper that turns filter values into params, and one that turns a link back into the search and filter values. The link helper returns `undefined` when the URL has none of the filter params and none of the table's (`dataTableQueryParamConfig`), so stored choices stay. Use a second controller on the page for them, as `ha-marketplace-dashboard.ts` and `lorawan-devices.ts` do. `browseUrl` and `browseLinkFromUrl` in `src/panels/marketplace/dashboards/dashboard-repositories.ts` show the pair.
|
||||
- Filter components also send the value the page gave them once they have looked up their items, so a filter event isn't always the user's. Store a filter only when it changes what the URL holds, comparing the filters' query strings before and after, as `helpersFilterQueryString` does, and store only that filter.
|
||||
- A memoized filter that also reads async state (anything loaded after the page connects) must take that state as a memoize argument, or a linked filter shows stale results until something else changes.
|
||||
- A filter component that looks up related items asynchronously should drop a lookup whose value has since changed, or two links landing within one lookup apply the older one.
|
||||
|
||||
## Coverage Check
|
||||
|
||||
Before calling a page done:
|
||||
|
||||
1. List every filter, sortable column, groupable column, the search box and any view toggle (such as a tab, raw view or provider). Confirm each one is written to the URL and read back from a link, both on first render and on a page that is already open.
|
||||
2. Search the codebase for in-app links and `navigate()` calls into the page with a query string, and move them to the current param names and the page's main path. `lorawan-connections.ts` and `lorawan-overview.ts` link into `lorawan-devices.ts` this way.
|
||||
|
||||
## My Home Assistant Links
|
||||
|
||||
When a page gains params, add them to its redirect in `getMyRedirects()` in `src/panels/my/ha-panel-my.ts`:
|
||||
|
||||
- List them under `params` as `"string?"`, matching what the page reads. For a data table page that includes `search`, `sort`, `direction` and `group` as well as its filters. Keep the list in a const next to the others, such as `marketplaceTableParams`, which several redirects share.
|
||||
- Point the redirect at the page's main path, the one the page's controller binds to.
|
||||
- Don't add legacy param names.
|
||||
- Add the same params to the redirect's entry in `redirect.json` in `home-assistant/my.home-assistant.io`, in a separate pull request that links the frontend pull request. Merge it only once the frontend pull request is in `dev`.
|
||||
- In the frontend pull request, link the My Home Assistant pull request under "Additional information": `- Link to My Home Assistant pull request: home-assistant/my.home-assistant.io#<n>`.
|
||||
|
||||
## Tests
|
||||
|
||||
Follow `test/common/url/query-params.test.ts`, `test/common/url/data-table-query-params.test.ts` and `test/common/controllers/query-params-controller.test.ts`. For a feature's params file, cover the encode and decode round trip, defaults being left out, and legacy names still being read. Test the helpers, not the page rendering, and only where the logic could break without anyone noticing.
|
||||
@@ -58,7 +58,7 @@ Readiness and invalidation:
|
||||
|
||||
Repository-owned contracts:
|
||||
|
||||
- Consult the public [frontend contributor documentation](https://contributors.home-assistant.io/frontend/) for documented architecture, data flow, design, and development workflows.
|
||||
- Consult the public [frontend developer documentation](https://developers.home-assistant.io/docs/frontend/) for documented architecture, data flow, design, and development workflows.
|
||||
- For new leaf components, load `ha-frontend-contexts` and verify they consume narrow contexts instead of introducing a broad `hass` property; containers and external APIs may still require `hass`.
|
||||
- Verify backend assumptions against the owning Core, Supervisor, or WebSocket implementation, and component assumptions against the exported component contract.
|
||||
- Prefer canonical repository helpers and test setup over duplicate local implementations.
|
||||
|
||||
@@ -15,45 +15,38 @@ not UCAndroid > 0
|
||||
# Floors are pinned explicitly (not usage-based) so the support policy is
|
||||
# deliberate and does not drift with global usage statistics, which do not
|
||||
# represent old tablets and wall displays used as Home Assistant dashboards:
|
||||
# - iOS/Safari >= 15.4: where native <dialog> (ha-drawer, popovers,
|
||||
# wa-dialog) landed. Dropped: the iOS 12 devices, iPad Air 1 / mini 2 /
|
||||
# mini 3, iPhone 5s / 6 / 6 Plus and iPod touch 6. Nothing ends between
|
||||
# iOS 12 and 15.8, so no other device is affected. On the Mac, Safari 15.4
|
||||
# runs on macOS 10.15 Catalina and later. The Chrome floor, not Safari,
|
||||
# decides what the legacy build has to transpile.
|
||||
# - Chrome >= 67: the first Chromium with BigInt, which Babel cannot
|
||||
# transpile. The boundary on Fire tablets is the Amazon WebView build, not
|
||||
# the Fire OS major: WebView 70+ (Fire OS 5.6.4+ on the 2017 models, Fire
|
||||
# OS 6.3.1.2+, Fire OS 7) keeps working, WebView 59 (Fire OS 5 on the 2015
|
||||
# and 2016 models, Fire OS 6.3.0.1) is dropped. Also covers the Chromecast
|
||||
# 1st gen (frozen at Chrome 70, the next hard ceiling) and old kiosk
|
||||
# webviews above it.
|
||||
# - iOS/Safari >= 12: iPad Air 1 / mini 2 / mini 3 (last supported iOS).
|
||||
# Older iPads (iPad 2/3/mini 1 on iOS 9.3, iPad 4 on iOS 10.3) cannot run
|
||||
# the app: their engines lack custom elements, shadow DOM, and/or CSS grid.
|
||||
# - Chrome >= 59: Fire OS 5 tablets (Fire 7/HD 8/HD 10 through ~2017) have
|
||||
# their system WebView pinned at Chromium 59 and commonly run Fully Kiosk;
|
||||
# also covers old kiosk browsers and no-longer-updating webviews above it.
|
||||
# - Edge >= 79: all Chromium-based Edge. Costs nothing (above the Chrome
|
||||
# floor); mainly catches Edge 109 on Windows 7/8.1 and update-frozen
|
||||
# enterprise installs, whose engines can run the legacy build fine.
|
||||
# - Firefox >= 98: where native <dialog> landed; otherwise a zero-cost floor,
|
||||
# not a chased population — pinning it adds no babel transforms, core-js
|
||||
# modules, or Lightning CSS prefixes to the output. Mozilla-supported Firefox (incl. current ESR) always matches
|
||||
# - Firefox >= 94: the zero-cost floor, not a chased population — pinning it
|
||||
# adds no babel transforms, core-js modules, or Lightning CSS prefixes to
|
||||
# the output. Mozilla-supported Firefox (incl. current ESR) always matches
|
||||
# [modern]; Firefox on old OSes is not supported (use Chrome 109 instead).
|
||||
# If Firefox ever becomes the pin forcing extra output, raise it first.
|
||||
# - Samsung >= 9: Samsung Internet on old Galaxy tablets (9.2 is Chromium 67)
|
||||
Chrome >= 67
|
||||
ChromeAndroid >= 67
|
||||
# - Samsung >= 9: Samsung Internet on old Galaxy tablets
|
||||
Chrome >= 59
|
||||
ChromeAndroid >= 59
|
||||
Edge >= 79
|
||||
Firefox >= 98
|
||||
FirefoxAndroid >= 98
|
||||
iOS >= 15.4
|
||||
Safari >= 15.4
|
||||
Firefox >= 94
|
||||
FirefoxAndroid >= 94
|
||||
iOS >= 12
|
||||
Safari >= 12
|
||||
Samsung >= 9
|
||||
|
||||
[legacy-sw]
|
||||
# Same as legacy, restricted to browsers that support service workers
|
||||
# (currently resolves to the same set; guards the service worker build if the legacy floor ever drops below them)
|
||||
Chrome >= 67 and supports serviceworkers
|
||||
ChromeAndroid >= 67 and supports serviceworkers
|
||||
Chrome >= 59 and supports serviceworkers
|
||||
ChromeAndroid >= 59 and supports serviceworkers
|
||||
Edge >= 79 and supports serviceworkers
|
||||
Firefox >= 98 and supports serviceworkers
|
||||
FirefoxAndroid >= 98 and supports serviceworkers
|
||||
iOS >= 15.4 and supports serviceworkers
|
||||
Safari >= 15.4 and supports serviceworkers
|
||||
Firefox >= 94 and supports serviceworkers
|
||||
FirefoxAndroid >= 94 and supports serviceworkers
|
||||
iOS >= 12 and supports serviceworkers
|
||||
Safari >= 12 and supports serviceworkers
|
||||
Samsung >= 9 and supports serviceworkers
|
||||
@@ -105,4 +105,4 @@ To help with the load of incoming pull requests:
|
||||
Below, some useful links you could explore:
|
||||
-->
|
||||
[docs-repository]: https://github.com/home-assistant/home-assistant.io
|
||||
[perfect-pr]: https://contributors.home-assistant.io/start/pull-requests/#before-you-open-it
|
||||
[perfect-pr]: https://developers.home-assistant.io/docs/review-process/#creating-the-perfect-pr
|
||||
@@ -34,7 +34,7 @@ runs:
|
||||
hashFiles('.nvmrc', 'package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml', 'patches/**') }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
|
||||
@@ -192,8 +192,8 @@ export default async function checkPullRequestStandards({
|
||||
`\n\n` +
|
||||
`Please complete the ` +
|
||||
`[PR template](https://github.com/home-assistant/frontend/blob/dev/.github/PULL_REQUEST_TEMPLATE.md?plain=1) ` +
|
||||
`and see the [contributor docs](https://contributors.home-assistant.io/start/pull-requests/#fill-out-the-template) ` +
|
||||
`for more on creating a great pull request.`;
|
||||
`and see the [developer docs](https://developers.home-assistant.io/docs/review-process) ` +
|
||||
`for more on creating a great pull request (see point 6).`;
|
||||
|
||||
if (existing) {
|
||||
await withRetry("comment update", () =>
|
||||
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
is-test: true
|
||||
rspack-cache: readonly
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: frontend-bundle-stats
|
||||
path: build/stats/*.json
|
||||
@@ -142,7 +142,7 @@ jobs:
|
||||
- name: Check entrypoint bundle size budget
|
||||
run: pnpm run check-bundlesize
|
||||
- name: Upload frontend build
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: frontend-build
|
||||
path: hass_frontend/
|
||||
|
||||
@@ -32,12 +32,12 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
with:
|
||||
languages: javascript-typescript
|
||||
build-mode: none
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
with:
|
||||
category: "/language:javascript-typescript"
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
name: Prepare container dependencies
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.64.1-noble
|
||||
image: mcr.microsoft.com/playwright:v1.63.0-noble
|
||||
options: --user 1001
|
||||
defaults:
|
||||
run:
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
is-test: true
|
||||
|
||||
- name: Upload demo build
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: demo-dist
|
||||
path: demo/dist/
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
is-test: true
|
||||
|
||||
- name: Upload e2e test app build
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-test-app-dist
|
||||
path: test/e2e/app/dist/
|
||||
@@ -140,7 +140,7 @@ jobs:
|
||||
is-test: true
|
||||
|
||||
- name: Upload gallery build
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: gallery-dist
|
||||
path: gallery/dist/
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
- prepare-container-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.64.1-noble
|
||||
image: mcr.microsoft.com/playwright:v1.63.0-noble
|
||||
options: --user 1001 --ipc=host
|
||||
defaults:
|
||||
run:
|
||||
@@ -182,7 +182,7 @@ jobs:
|
||||
node-modules-cache-key: node-modules-container-v1
|
||||
|
||||
- name: Download demo build
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: demo-dist
|
||||
path: demo/dist/
|
||||
@@ -192,7 +192,7 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
|
||||
- name: Upload demo blob report
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: always()
|
||||
with:
|
||||
name: blob-report-demo-${{ matrix.shardIndex }}
|
||||
@@ -207,7 +207,7 @@ jobs:
|
||||
- prepare-container-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.64.1-noble
|
||||
image: mcr.microsoft.com/playwright:v1.63.0-noble
|
||||
options: --user 1001 --ipc=host
|
||||
defaults:
|
||||
run:
|
||||
@@ -236,7 +236,7 @@ jobs:
|
||||
node-modules-cache-key: node-modules-container-v1
|
||||
|
||||
- name: Download e2e test app build
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: e2e-test-app-dist
|
||||
path: test/e2e/app/dist/
|
||||
@@ -246,7 +246,7 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
|
||||
- name: Upload app blob report
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: always()
|
||||
with:
|
||||
name: blob-report-app-${{ matrix.shardIndex }}
|
||||
@@ -261,7 +261,7 @@ jobs:
|
||||
- prepare-container-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.64.1-noble
|
||||
image: mcr.microsoft.com/playwright:v1.63.0-noble
|
||||
options: --user 1001 --ipc=host
|
||||
defaults:
|
||||
run:
|
||||
@@ -290,7 +290,7 @@ jobs:
|
||||
node-modules-cache-key: node-modules-container-v1
|
||||
|
||||
- name: Download gallery build
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: gallery-dist
|
||||
path: gallery/dist/
|
||||
@@ -300,7 +300,7 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
|
||||
- name: Upload gallery blob report
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: always()
|
||||
with:
|
||||
name: blob-report-gallery-${{ matrix.shardIndex }}
|
||||
@@ -331,21 +331,21 @@ jobs:
|
||||
node-modules-cache: true
|
||||
|
||||
- name: Download demo blob reports
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
continue-on-error: true
|
||||
with:
|
||||
pattern: blob-report-demo-*
|
||||
path: test/e2e/reports/demo/
|
||||
|
||||
- name: Download app blob reports
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
continue-on-error: true
|
||||
with:
|
||||
pattern: blob-report-app-*
|
||||
path: test/e2e/reports/app/
|
||||
|
||||
- name: Download gallery blob reports
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
continue-on-error: true
|
||||
with:
|
||||
pattern: blob-report-gallery-*
|
||||
@@ -359,7 +359,7 @@ jobs:
|
||||
|
||||
- name: Upload merged HTML report
|
||||
id: upload-report
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-report
|
||||
path: test/e2e/reports/combined/
|
||||
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
key: rspack-cache-${{ runner.os }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: wheels
|
||||
path: dist/home_assistant_frontend*.whl
|
||||
@@ -137,7 +137,7 @@ jobs:
|
||||
run: tar -czvf translations.tar.gz translations
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send bundle stats and build information to RelativeCI
|
||||
uses: relative-ci/agent-action@711f3a8fa1b928e6afde280011110c9b0a224a60 # v3.2.4
|
||||
uses: relative-ci/agent-action@fcf45416581928e8dd62eded78ce98c78e5149f8 # v3.2.3
|
||||
with:
|
||||
key: ${{ secrets.RELATIVE_CI_KEY_frontend_modern }}
|
||||
token: ${{ github.token }}
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send bundle stats and build information to RelativeCI
|
||||
uses: relative-ci/agent-action@711f3a8fa1b928e6afde280011110c9b0a224a60 # v3.2.4
|
||||
uses: relative-ci/agent-action@fcf45416581928e8dd62eded78ce98c78e5149f8 # v3.2.3
|
||||
with:
|
||||
key: ${{ secrets.RELATIVE_CI_KEY_frontend_legacy }}
|
||||
token: ${{ github.token }}
|
||||
|
||||
@@ -19,6 +19,6 @@ jobs:
|
||||
pull-requests: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@978bb0b30c5ee8cdd332ced253758157150d43f0 # v7.10.0
|
||||
- uses: release-drafter/release-drafter@34d80673e067bdc0c24568d3af899c216adcfaa9 # v7.7.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@cd6f3635a740c84d8a14f6121a90068b8ece6bf6 # master
|
||||
uses: home-assistant/actions/helpers/verify-version@06749dd8c0b54f350bc69c8752456cee498808a3 # master
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
@@ -15,12 +15,11 @@ jobs:
|
||||
if: github.repository == 'home-assistant/frontend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Stale policy
|
||||
- name: 90 days stale policy
|
||||
uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-issue-stale: 90
|
||||
days-before-pr-stale: 60
|
||||
days-before-stale: 90
|
||||
days-before-close: 7
|
||||
operations-per-run: 25
|
||||
remove-stale-when-updated: true
|
||||
|
||||
@@ -67,3 +67,4 @@ test/e2e/app/dist/
|
||||
.serena
|
||||
|
||||
test/benchmarks/results/
|
||||
|
||||
@@ -46,7 +46,6 @@ Detailed guidance lives in `.agents/skills/<name>/SKILL.md`. Load every matching
|
||||
- `ha-frontend-lit`: reactive fields, DOM queries, lifecycle behavior, and render-derived state.
|
||||
- `ha-frontend-styling`: theme variables, spacing tokens, responsive layout, RTL, and view transitions.
|
||||
- `ha-frontend-testing`: lint, typecheck, Vitest, Playwright e2e dev servers, and benchmarks.
|
||||
- `ha-frontend-query-params`: typed URL query params, `QueryParamsController`, data table URL sync, in-app links, and My Home Assistant redirect params.
|
||||
- `ha-frontend-user-facing-text`: localization, terminology, sentence case, and Home Assistant text style.
|
||||
- `ha-frontend-review`: PR template use, existing review feedback, review checklist, recurring issues, and UI/UX evidence including gallery specifications.
|
||||
- `ha-frontend-ux-readiness`: UI/UX routing that complements frontend review for new user experiences.
|
||||
|
||||
@@ -41,5 +41,5 @@ Contributions that do not follow this policy will be closed. Repeated violations
|
||||
---
|
||||
|
||||
The canonical version of this policy is published at
|
||||
<https://contributors.home-assistant.io/start/ai-policy/>. In case of differences,
|
||||
<https://developers.home-assistant.io/docs/ai_policy>. In case of differences,
|
||||
the published version applies.
|
||||
@@ -6,12 +6,12 @@ This is the repository for the official [Home Assistant](https://home-assistant.
|
||||
|
||||
- [View demo of Home Assistant](https://demo.home-assistant.io/)
|
||||
- [More information about Home Assistant](https://home-assistant.io)
|
||||
- [Frontend development instructions](https://contributors.home-assistant.io/frontend/development/)
|
||||
- [Frontend development instructions](https://developers.home-assistant.io/docs/frontend/development/)
|
||||
|
||||
## Development
|
||||
|
||||
- Initial setup: `script/setup`
|
||||
- Development: [Instructions](https://contributors.home-assistant.io/frontend/development/)
|
||||
- Development: [Instructions](https://developers.home-assistant.io/docs/frontend/development/)
|
||||
- Production build: `pnpm build`
|
||||
- Gallery: `cd gallery && script/develop_gallery`
|
||||
|
||||
|
||||
@@ -18,6 +18,17 @@ const polyfillSupport = {
|
||||
safari: 17.4,
|
||||
samsung: 15.0,
|
||||
},
|
||||
"element-getattributenames": {
|
||||
android: 61,
|
||||
chrome: 61,
|
||||
edge: 18,
|
||||
firefox: 45,
|
||||
ios: 10.3,
|
||||
opera: 48,
|
||||
opera_mobile: 45,
|
||||
safari: 10.1,
|
||||
samsung: 8.0,
|
||||
},
|
||||
"element-toggleattribute": {
|
||||
android: 69,
|
||||
chrome: 69,
|
||||
@@ -56,20 +67,39 @@ const polyfillSupport = {
|
||||
"intl-other": {
|
||||
// Not specified (i.e. always try polyfill) since compatibility depends on supported locales
|
||||
},
|
||||
"resize-observer": {
|
||||
android: 64,
|
||||
chrome: 64,
|
||||
edge: 79,
|
||||
firefox: 69,
|
||||
ios: 13.4,
|
||||
opera: 51,
|
||||
opera_mobile: 47,
|
||||
safari: 13.1,
|
||||
samsung: 9.0,
|
||||
},
|
||||
};
|
||||
|
||||
// Map of instance and static properties to the corresponding polyfill key
|
||||
// and actual module to import
|
||||
// Map of global variables and/or instance and static properties to the
|
||||
// corresponding polyfill key and actual module to import
|
||||
const polyfillMap = {
|
||||
global: {
|
||||
ResizeObserver: {
|
||||
key: "resize-observer",
|
||||
module: join(POLYFILL_DIR, "resize-observer.ts"),
|
||||
},
|
||||
},
|
||||
instance: {
|
||||
attachInternals: {
|
||||
key: "element-internals",
|
||||
module: "element-internals-polyfill",
|
||||
},
|
||||
toggleAttribute: {
|
||||
key: "element-toggleattribute",
|
||||
module: join(POLYFILL_DIR, "element-toggleattribute.ts"),
|
||||
},
|
||||
...Object.fromEntries(
|
||||
["getAttributeNames", "toggleAttribute"].map((prop) => {
|
||||
const key = `element-${prop.toLowerCase()}`;
|
||||
return [prop, { key, module: join(POLYFILL_DIR, `${key}.ts`) }];
|
||||
})
|
||||
),
|
||||
},
|
||||
static: {
|
||||
Intl: {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"_comment": "Initial JS budget (raw/uncompressed bytes) for the cold-load critical entrypoints. Enforced by build-scripts/check-bundle-size.cjs in CI. Re-seed after an intentional change with `--update --headroom=<percent>`.",
|
||||
"frontend-modern": {
|
||||
"app": 627654,
|
||||
"core": 55147,
|
||||
"authorize": 593862,
|
||||
"onboarding": 709255
|
||||
"app": 624240,
|
||||
"core": 55144,
|
||||
"authorize": 594400,
|
||||
"onboarding": 705499
|
||||
},
|
||||
"frontend-legacy": {
|
||||
"app": 724225,
|
||||
"core": 157026,
|
||||
"authorize": 693913,
|
||||
"onboarding": 812415
|
||||
"app": 776696,
|
||||
"core": 188309,
|
||||
"authorize": 763316,
|
||||
"onboarding": 879807
|
||||
}
|
||||
}
|
||||
@@ -96,9 +96,8 @@ module.exports.htmlMinifierOptions = {
|
||||
module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({
|
||||
// Highest syntax the minifier may emit; it never downlevels. Every browser
|
||||
// in [modern] is well past ES2020 (universal since spring 2020); the
|
||||
// [legacy] floors (Chrome 67 / Safari 15) top out at ES2019: optional
|
||||
// catch binding is in, `?.` and `??` (Chrome 80) are not.
|
||||
ecma: latestBuild ? 2020 : 2019,
|
||||
// [legacy] floors (Chrome 59 / Safari 12) top out at ES2017.
|
||||
ecma: latestBuild ? 2020 : 2017,
|
||||
module: latestBuild,
|
||||
format: { comments: false },
|
||||
sourceMap: !isTestBuild,
|
||||
@@ -186,6 +185,7 @@ module.exports.babelOptions = ({ latestBuild, isTestBuild, sw, worker }) => ({
|
||||
path.join(paths.root_dir, "src/resources/polyfills"),
|
||||
...[
|
||||
"@formatjs/(?:ecma402-abstract|intl-\\w+)",
|
||||
"@lit-labs/virtualizer/polyfills",
|
||||
"@webcomponents/scoped-custom-element-registry",
|
||||
"element-internals-polyfill",
|
||||
].map((p) => new RegExp(`/node_modules/${p}/`)),
|
||||
|
||||
@@ -2,14 +2,10 @@
|
||||
|
||||
import fs from "fs-extra";
|
||||
import gulp from "gulp";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "path";
|
||||
import paths from "../paths.cjs";
|
||||
import { ensureMapAssets, mapAssetsDir } from "./map-assets.js";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const npmPath = (...parts) =>
|
||||
path.resolve(paths.root_dir, "node_modules", ...parts);
|
||||
const polyPath = (...parts) => path.resolve(paths.root_dir, ...parts);
|
||||
@@ -87,24 +83,9 @@ async function copyMapPanel(staticDir) {
|
||||
|
||||
function copyZXingWasm(staticDir) {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
// Resolve through barcode-detector, the wasm must match the zxing-wasm JS it bundles
|
||||
const zxingWasm = createRequire(require.resolve("barcode-detector")).resolve(
|
||||
"zxing-wasm/reader/zxing_reader.wasm"
|
||||
);
|
||||
const {
|
||||
ZXING_WASM_SHA256,
|
||||
ZXING_WASM_VERSION,
|
||||
} = require("barcode-detector/pure");
|
||||
const wasm = fs.readFileSync(zxingWasm);
|
||||
if (createHash("sha256").update(wasm).digest("hex") !== ZXING_WASM_SHA256) {
|
||||
throw new Error(
|
||||
`${zxingWasm} does not match the zxing-wasm ${ZXING_WASM_VERSION} bundled in barcode-detector`
|
||||
);
|
||||
}
|
||||
// Versioned file name, see locateFile in ha-qr-scanner
|
||||
fs.outputFileSync(
|
||||
staticPath("js", `zxing_reader-${ZXING_WASM_VERSION}.wasm`),
|
||||
wasm
|
||||
copyFileDir(
|
||||
npmPath("zxing-wasm/dist/reader/zxing_reader.wasm"),
|
||||
staticPath("js")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -153,9 +153,7 @@ gulp.task("rspack-watch-app", () => {
|
||||
: createAppConfig({ isProdBuild: false, latestBuild: true })
|
||||
).watch({ poll: isWsl }, doneHandler());
|
||||
gulp.watch(
|
||||
// Watch the folder via a glob; git replaces en.json on checkout/stash,
|
||||
// which can permanently drop a watch on the file itself
|
||||
path.join(paths.translations_src, "*.json"),
|
||||
path.join(paths.translations_src, "en.json"),
|
||||
gulp.series("build-translations", "copy-translations-app")
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ const fs = require("fs");
|
||||
const { existsSync } = fs;
|
||||
const path = require("path");
|
||||
const rspack = require("@rspack/core");
|
||||
const browserslist = require("browserslist");
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { RsdoctorRspackPlugin } = require("@rsdoctor/rspack-plugin");
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
@@ -41,21 +40,6 @@ const TOOLCHAIN_PACKAGES = [
|
||||
"caniuse-lite",
|
||||
];
|
||||
|
||||
// Not direct dependencies: read the copy the package that loads them uses.
|
||||
const TOOLCHAIN_PACKAGE_PARENTS = {
|
||||
terser: "terser-webpack-plugin",
|
||||
"caniuse-lite": "browserslist",
|
||||
};
|
||||
|
||||
const toolchainVersion = (pkg) => {
|
||||
const parent = TOOLCHAIN_PACKAGE_PARENTS[pkg];
|
||||
const lookup = parent && [
|
||||
path.dirname(require.resolve(`${parent}/package.json`)),
|
||||
];
|
||||
return require(require.resolve(`${pkg}/package.json`, { paths: lookup }))
|
||||
.version;
|
||||
};
|
||||
|
||||
// Our own build logic — the config, loaders and babel plugins. Their contents
|
||||
// (not their paths) go into the cache version, so a change invalidates the
|
||||
// cache the same way `buildDependencies` would, but without tying validity to
|
||||
@@ -79,7 +63,9 @@ const PATCHES_DIR = path.join(paths.root_dir, "patches");
|
||||
// stays valid when reused on a different machine or checkout path.
|
||||
const cacheVersion = () => {
|
||||
const parts = [
|
||||
...TOOLCHAIN_PACKAGES.map((pkg) => `${pkg}@${toolchainVersion(pkg)}`),
|
||||
...TOOLCHAIN_PACKAGES.map(
|
||||
(pkg) => `${pkg}@${require(`${pkg}/package.json`).version}`
|
||||
),
|
||||
...CONFIG_FILES.map(
|
||||
(file) => `${path.basename(file)}:${fs.readFileSync(file, "utf8")}`
|
||||
),
|
||||
@@ -129,10 +115,7 @@ const createRspackConfig = ({
|
||||
dontHash = new Set();
|
||||
}
|
||||
const ignorePackages = bundle.ignorePackages({ latestBuild });
|
||||
// pnpm links node_modules/lit-html into its store; rspack compares real paths
|
||||
const litHtmlRoot = fs.realpathSync(
|
||||
path.resolve(__dirname, "../node_modules/lit-html")
|
||||
);
|
||||
const litHtmlRoot = path.resolve(__dirname, "../node_modules/lit-html");
|
||||
const litHtmlDevelopmentRoot = path.join(litHtmlRoot, "development");
|
||||
const litDisableDevModeLoader = path.join(
|
||||
__dirname,
|
||||
@@ -229,22 +212,6 @@ const createRspackConfig = ({
|
||||
{
|
||||
test: /\.css$/,
|
||||
type: "asset/source",
|
||||
// Imported stylesheets skip the minifier that compiles css``
|
||||
// templates, so legacy builds compile logical properties and :is()
|
||||
// here
|
||||
use: latestBuild
|
||||
? []
|
||||
: [
|
||||
{
|
||||
loader: "builtin:lightningcss-loader",
|
||||
options: {
|
||||
targets: browserslist.loadConfig({
|
||||
path: paths.root_dir,
|
||||
env: "legacy",
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -410,14 +377,6 @@ const createRspackConfig = ({
|
||||
resolve: {
|
||||
extensions: [".ts", ".js", ".json"],
|
||||
alias: {
|
||||
// @formatjs/bigdecimal uses BigInt (Safari 14, Chrome 67), which
|
||||
// breaks the NumberFormat and PluralRules polyfills
|
||||
...(!latestBuild && {
|
||||
"@formatjs/bigdecimal$": path.join(
|
||||
paths.root_dir,
|
||||
"src/resources/polyfills/bigdecimal.ts"
|
||||
),
|
||||
}),
|
||||
"lit/static-html$": "lit/static-html.js",
|
||||
"lit/decorators$": "lit/decorators.js",
|
||||
"lit/directive$": "lit/directive.js",
|
||||
@@ -434,6 +393,8 @@ const createRspackConfig = ({
|
||||
"lit/directives/keyed$": "lit/directives/keyed.js",
|
||||
"@lit-labs/virtualizer/layouts/grid":
|
||||
"@lit-labs/virtualizer/layouts/grid.js",
|
||||
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver":
|
||||
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
|
||||
"@lit-labs/observers/resize-controller":
|
||||
"@lit-labs/observers/resize-controller.js",
|
||||
"@formatjs/intl-durationformat/should-polyfill$":
|
||||
|
||||
@@ -4,18 +4,23 @@ const castContext = framework.CastReceiverContext.getInstance();
|
||||
|
||||
const playerManager = castContext.getPlayerManager();
|
||||
|
||||
playerManager.setMessageInterceptor("LOAD", (loadRequestData) => {
|
||||
const media = loadRequestData.media;
|
||||
// Special handling if it came from Google Assistant
|
||||
if (media.entity) {
|
||||
media.contentId = media.entity;
|
||||
media.streamType = "LIVE";
|
||||
media.contentType = "application/vnd.apple.mpegurl";
|
||||
// @ts-expect-error type definition is wrong, should be "FMP4" instead of "fmp4"
|
||||
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.HlsVideoSegmentFormat
|
||||
media.hlsVideoSegmentFormat = "FMP4";
|
||||
playerManager.setMessageInterceptor(
|
||||
"LOAD" as framework.messages.MessageType.LOAD,
|
||||
(loadRequestData) => {
|
||||
const media = loadRequestData.media;
|
||||
// Special handling if it came from Google Assistant
|
||||
if (media.entity) {
|
||||
media.contentId = media.entity;
|
||||
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
|
||||
media.contentType = "application/vnd.apple.mpegurl";
|
||||
// @ts-ignore
|
||||
// type definition is wrong, should be "FMP4" instead of "fmp4"
|
||||
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.HlsVideoSegmentFormat
|
||||
media.hlsVideoSegmentFormat =
|
||||
"FMP4" as framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
}
|
||||
return loadRequestData;
|
||||
}
|
||||
return loadRequestData;
|
||||
});
|
||||
);
|
||||
|
||||
castContext.start();
|
||||
@@ -4,7 +4,7 @@ import { castContext } from "../cast_context";
|
||||
|
||||
export const castDemoLovelace: () => LovelaceConfig = () => {
|
||||
const touchSupported =
|
||||
castContext.getDeviceCapabilities()?.touch_input_supported;
|
||||
castContext.getDeviceCapabilities().touch_input_supported;
|
||||
return {
|
||||
views: [
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@ const playerStylesAdded = false;
|
||||
let controls: HTMLElement | null;
|
||||
|
||||
const setTouchControlsVisibility = (visible: boolean) => {
|
||||
if (!castContext.getDeviceCapabilities()?.touch_input_supported) {
|
||||
if (!castContext.getDeviceCapabilities().touch_input_supported) {
|
||||
return;
|
||||
}
|
||||
controls =
|
||||
@@ -39,7 +39,8 @@ const playDummyMedia = (viewTitle?: string) => {
|
||||
loadRequestData.media.contentId =
|
||||
"https://cast.home-assistant.io/images/google-nest-hub.png";
|
||||
loadRequestData.media.contentType = "image/jpeg";
|
||||
loadRequestData.media.streamType = "NONE";
|
||||
loadRequestData.media.streamType =
|
||||
"NONE" as framework.messages.StreamType.NONE;
|
||||
const metadata = new framework.messages.GenericMediaMetadata();
|
||||
metadata.title = viewTitle;
|
||||
loadRequestData.media.metadata = metadata;
|
||||
@@ -50,7 +51,7 @@ const playDummyMedia = (viewTitle?: string) => {
|
||||
clearTimeout(timeOut);
|
||||
timeOut = undefined;
|
||||
}
|
||||
if (castContext.getDeviceCapabilities()?.touch_input_supported) {
|
||||
if (castContext.getDeviceCapabilities().touch_input_supported) {
|
||||
timeOut = window.setTimeout(() => playDummyMedia(viewTitle), 540000); // repeat every 9 minutes to keep it active (gets deactivated after 10 minutes)
|
||||
}
|
||||
};
|
||||
@@ -90,12 +91,15 @@ options.disableIdleTimeout = true;
|
||||
options.customNamespaces = {
|
||||
// type definition is wrong, should be "JSON" instead of "json"
|
||||
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.system#.MessageType
|
||||
[CAST_NS]: "JSON",
|
||||
[CAST_NS]: "JSON" as framework.system.MessageType.JSON,
|
||||
};
|
||||
|
||||
castContext.addCustomMessageListener(CAST_NS, (ev) => {
|
||||
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
|
||||
if (playerManager.getPlayerState() !== "IDLE") {
|
||||
if (
|
||||
playerManager.getPlayerState() !==
|
||||
("IDLE" as framework.messages.PlayerState.IDLE)
|
||||
) {
|
||||
playerManager.stop();
|
||||
} else {
|
||||
showLovelaceController();
|
||||
@@ -107,37 +111,46 @@ castContext.addCustomMessageListener(CAST_NS, (ev) => {
|
||||
|
||||
const playerManager = castContext.getPlayerManager();
|
||||
|
||||
playerManager.setMessageInterceptor("LOAD", (loadRequestData) => {
|
||||
if (
|
||||
loadRequestData.media.contentId ===
|
||||
"https://cast.home-assistant.io/images/google-nest-hub.png"
|
||||
) {
|
||||
playerManager.setMessageInterceptor(
|
||||
"LOAD" as framework.messages.MessageType.LOAD,
|
||||
(loadRequestData) => {
|
||||
if (
|
||||
loadRequestData.media.contentId ===
|
||||
"https://cast.home-assistant.io/images/google-nest-hub.png"
|
||||
) {
|
||||
return loadRequestData;
|
||||
}
|
||||
// We received a play media command, hide Lovelace and show media player
|
||||
showMediaPlayer();
|
||||
const media = loadRequestData.media;
|
||||
// Special handling if it came from Google Assistant
|
||||
if (media.entity) {
|
||||
media.contentId = media.entity;
|
||||
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
|
||||
media.contentType = "application/vnd.apple.mpegurl";
|
||||
// type definition is wrong, should be "FMP4" instead of "fmp4"
|
||||
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.HlsVideoSegmentFormat
|
||||
media.hlsVideoSegmentFormat =
|
||||
"FMP4" as framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
}
|
||||
return loadRequestData;
|
||||
}
|
||||
// We received a play media command, hide Lovelace and show media player
|
||||
showMediaPlayer();
|
||||
const media = loadRequestData.media;
|
||||
// Special handling if it came from Google Assistant
|
||||
if (media.entity) {
|
||||
media.contentId = media.entity;
|
||||
media.streamType = "LIVE";
|
||||
media.contentType = "application/vnd.apple.mpegurl";
|
||||
// @ts-expect-error type definition is wrong, should be "FMP4" instead of "fmp4"
|
||||
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.HlsVideoSegmentFormat
|
||||
media.hlsVideoSegmentFormat = "FMP4";
|
||||
}
|
||||
return loadRequestData;
|
||||
});
|
||||
);
|
||||
|
||||
playerManager.addEventListener("MEDIA_STATUS", (event) => {
|
||||
if (
|
||||
event.mediaStatus?.playerState === "IDLE" &&
|
||||
event.mediaStatus?.idleReason &&
|
||||
event.mediaStatus?.idleReason !== "INTERRUPTED"
|
||||
) {
|
||||
// media finished or stopped, return to default Lovelace
|
||||
showLovelaceController();
|
||||
playerManager.addEventListener(
|
||||
"MEDIA_STATUS" as framework.events.EventType.MEDIA_STATUS,
|
||||
(event) => {
|
||||
if (
|
||||
event.mediaStatus?.playerState ===
|
||||
("IDLE" as framework.messages.PlayerState.IDLE) &&
|
||||
event.mediaStatus?.idleReason &&
|
||||
event.mediaStatus?.idleReason !==
|
||||
("INTERRUPTED" as framework.messages.IdleReason.INTERRUPTED)
|
||||
) {
|
||||
// media finished or stopped, return to default Lovelace
|
||||
showLovelaceController();
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
castContext.start(options);
|
||||
@@ -27,7 +27,6 @@ import type {
|
||||
LovelaceConfig,
|
||||
LovelaceDashboardStrategyConfig,
|
||||
} from "../../../../src/data/lovelace/config/types";
|
||||
import { normalizeLovelaceConfig } from "../../../../src/data/lovelace/config/normalize";
|
||||
import { isStrategyDashboard } from "../../../../src/data/lovelace/config/types";
|
||||
import { fetchResources } from "../../../../src/data/lovelace/resource";
|
||||
import { loadLovelaceResources } from "../../../../src/panels/lovelace/common/load-resources";
|
||||
@@ -35,6 +34,7 @@ import { HassElement } from "../../../../src/state/hass-element";
|
||||
import { castContext } from "../cast_context";
|
||||
import "./hc-launch-screen";
|
||||
import { getPanelTitleFromUrlPath } from "../../../../src/data/panel";
|
||||
import { checkLovelaceConfig } from "../../../../src/panels/lovelace/common/check-lovelace-config";
|
||||
|
||||
const DEFAULT_CONFIG: LovelaceDashboardStrategyConfig = {
|
||||
strategy: {
|
||||
@@ -359,7 +359,7 @@ export class HcMain extends HassElement {
|
||||
this._urlPath || "lovelace"
|
||||
);
|
||||
castContext.setApplicationState(title || "");
|
||||
this._lovelaceConfig = normalizeLovelaceConfig(
|
||||
this._lovelaceConfig = checkLovelaceConfig(
|
||||
lovelaceConfig
|
||||
) as LovelaceConfig;
|
||||
}
|
||||
|
||||
@@ -142,45 +142,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
(localize?: LocalizeFunc) => localize ?? this.hass!.localize
|
||||
);
|
||||
|
||||
mockLovelace(hass, localizePromise, [
|
||||
{
|
||||
id: "music",
|
||||
url_path: "music",
|
||||
require_admin: false,
|
||||
show_in_sidebar: true,
|
||||
icon: "mdi:music",
|
||||
title: "Music",
|
||||
mode: "storage",
|
||||
},
|
||||
{
|
||||
id: "cameras",
|
||||
url_path: "cameras",
|
||||
require_admin: false,
|
||||
show_in_sidebar: true,
|
||||
icon: "mdi:cctv",
|
||||
title: "Cameras",
|
||||
mode: "storage",
|
||||
},
|
||||
{
|
||||
id: "garden",
|
||||
url_path: "garden",
|
||||
require_admin: false,
|
||||
show_in_sidebar: true,
|
||||
icon: "mdi:flower",
|
||||
title: "Garden",
|
||||
mode: "storage",
|
||||
},
|
||||
{
|
||||
id: "lovelace-advanced",
|
||||
url_path: "lovelace-advanced",
|
||||
require_admin: false,
|
||||
show_in_sidebar: false,
|
||||
icon: "mdi:cog",
|
||||
title: "Advanced",
|
||||
mode: "yaml",
|
||||
filename: "lovelace-advanced.yaml",
|
||||
},
|
||||
]);
|
||||
mockLovelace(hass, localizePromise);
|
||||
mockAuth(hass);
|
||||
mockTranslations(hass, localizePromise);
|
||||
mockHistory(hass);
|
||||
@@ -210,35 +172,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
mockAreaRegistry(hass);
|
||||
mockDeviceRegistry(hass, demoDevices);
|
||||
mockFloorRegistry(hass);
|
||||
mockLabelRegistry(hass, [
|
||||
{
|
||||
label_id: "kitchen",
|
||||
name: "Kitchen",
|
||||
icon: null,
|
||||
color: "red",
|
||||
description: "Everything in the kitchen",
|
||||
created_at: 1735689600,
|
||||
modified_at: 1735689600,
|
||||
},
|
||||
{
|
||||
label_id: "garden",
|
||||
name: "Garden",
|
||||
icon: "mdi:flower",
|
||||
color: "green",
|
||||
description: null,
|
||||
created_at: 1735689600,
|
||||
modified_at: 1735689600,
|
||||
},
|
||||
{
|
||||
label_id: "battery-powered",
|
||||
name: "Battery powered",
|
||||
icon: null,
|
||||
color: "orange",
|
||||
description: "Runs on batteries",
|
||||
created_at: 1735689600,
|
||||
modified_at: 1735689600,
|
||||
},
|
||||
]);
|
||||
mockLabelRegistry(hass);
|
||||
mockUsagePrediction(hass);
|
||||
mockEntityRegistryDisplay(hass);
|
||||
mockEntityRegistry(hass, [
|
||||
|
||||
@@ -39,9 +39,6 @@
|
||||
html {
|
||||
background-color: var(--primary-background-color, #fafafa);
|
||||
color: var(--primary-text-color, #212121);
|
||||
/* Prevent iOS Safari from inflating text when rotating to landscape */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
|
||||
@@ -39,7 +39,6 @@ const backupInfo: BackupInfo = {
|
||||
},
|
||||
],
|
||||
agent_errors: {},
|
||||
agent_error_translations: {},
|
||||
last_attempted_automatic_backup: recent,
|
||||
last_completed_automatic_backup: recent,
|
||||
last_action_event: { manager_state: "idle" },
|
||||
|
||||
@@ -42,12 +42,4 @@ export const mockBlueprint = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("blueprint/list", (msg: { domain: BlueprintDomain }) =>
|
||||
msg.domain === "script" ? scriptBlueprints : automationBlueprints
|
||||
);
|
||||
hass.mockWS(
|
||||
"blueprint/delete",
|
||||
(msg: { domain: BlueprintDomain; path: string }) => {
|
||||
const blueprints =
|
||||
msg.domain === "script" ? scriptBlueprints : automationBlueprints;
|
||||
delete blueprints[msg.path];
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import { lorawanFixtures } from "./lorawan/fixtures";
|
||||
import { bluetoothFixtures } from "./bluetooth/fixtures";
|
||||
import { infraredFixtures } from "./infrared/fixtures";
|
||||
import { matterFixtures } from "./matter/fixtures";
|
||||
@@ -16,7 +15,6 @@ import { zwaveJsFixtures } from "./zwave_js/fixtures";
|
||||
// data to mock. Each owns its own fixtures, so they can be added and removed
|
||||
// one at a time.
|
||||
const INTEGRATIONS: ConnectivityFixtures[] = [
|
||||
lorawanFixtures,
|
||||
bluetoothFixtures,
|
||||
serialFixtures,
|
||||
modbusFixtures,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { mockLoRaWAN } from "./lorawan/mock";
|
||||
import type { MockHomeAssistant } from "../../../../src/fake_data/provide_hass";
|
||||
import { mockBluetooth } from "./bluetooth/mock";
|
||||
import { mockMatter } from "./matter/mock";
|
||||
@@ -12,7 +11,6 @@ import { mockZwaveJs } from "./zwave_js/mock";
|
||||
|
||||
// The WebSocket mocks, code-split into the config panel chunk.
|
||||
const MOCKS = [
|
||||
mockLoRaWAN,
|
||||
mockBluetooth,
|
||||
mockSerial,
|
||||
mockModbus,
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
import { manifest } from "../../manifest";
|
||||
import {
|
||||
configEntry,
|
||||
device,
|
||||
registryEntry,
|
||||
withRegistryLinks,
|
||||
} from "../helpers";
|
||||
import type { ConnectivityFixtures } from "../types";
|
||||
|
||||
export const CHIRPSTACK_ENTRY = "mock-lorawan-chirpstack";
|
||||
export const TTN_ENTRY = "mock-lorawan-ttn";
|
||||
export const SENSECAP_ENTRY = "mock-lorawan-sensecap";
|
||||
|
||||
const REGISTRY_ENTRIES = [
|
||||
registryEntry(
|
||||
"sensor.lorawan_greenhouse_temperature",
|
||||
"lorawan-greenhouse",
|
||||
SENSECAP_ENTRY,
|
||||
"sensecap_lorawan",
|
||||
"Temperature"
|
||||
),
|
||||
registryEntry(
|
||||
"sensor.lorawan_greenhouse_humidity",
|
||||
"lorawan-greenhouse",
|
||||
SENSECAP_ENTRY,
|
||||
"sensecap_lorawan",
|
||||
"Humidity"
|
||||
),
|
||||
registryEntry(
|
||||
"sensor.lorawan_study_carbon_dioxide",
|
||||
"lorawan-study",
|
||||
SENSECAP_ENTRY,
|
||||
"sensecap_lorawan",
|
||||
"Carbon dioxide"
|
||||
),
|
||||
];
|
||||
|
||||
export const lorawanFixtures: ConnectivityFixtures = {
|
||||
components: [
|
||||
"lorawan",
|
||||
"sensecap_lorawan",
|
||||
"chirpstack",
|
||||
"thethingsnetwork_lorawan",
|
||||
],
|
||||
commands: ["lorawan/"],
|
||||
manifests: [
|
||||
manifest("lorawan", "LoRaWAN", { integration_type: "helper" }),
|
||||
manifest("sensecap_lorawan", "SenseCAP", {
|
||||
integration_type: "device",
|
||||
dependencies: ["lorawan"],
|
||||
}),
|
||||
manifest("chirpstack", "ChirpStack", { integration_type: "hub" }),
|
||||
manifest("thethingsnetwork_lorawan", "The Things Network", {
|
||||
integration_type: "hub",
|
||||
}),
|
||||
],
|
||||
configEntries: [
|
||||
{
|
||||
type: "hub",
|
||||
entry: configEntry(CHIRPSTACK_ENTRY, "chirpstack", "Home ChirpStack"),
|
||||
},
|
||||
{
|
||||
type: "hub",
|
||||
entry: configEntry(
|
||||
TTN_ENTRY,
|
||||
"thethingsnetwork_lorawan",
|
||||
"The Things Network"
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "device",
|
||||
entry: configEntry(SENSECAP_ENTRY, "sensecap_lorawan", "SenseCAP"),
|
||||
},
|
||||
],
|
||||
devices: [
|
||||
device(
|
||||
"lorawan-greenhouse",
|
||||
"Greenhouse",
|
||||
"Seeed Studio",
|
||||
"S2101",
|
||||
SENSECAP_ENTRY,
|
||||
{
|
||||
config_entries: [SENSECAP_ENTRY, CHIRPSTACK_ENTRY],
|
||||
identifiers: [
|
||||
["sensecap_lorawan", `${CHIRPSTACK_ENTRY}:0000000000000001`],
|
||||
],
|
||||
}
|
||||
),
|
||||
device(
|
||||
"lorawan-study",
|
||||
"Study air quality",
|
||||
"Seeed Studio",
|
||||
"S2103",
|
||||
SENSECAP_ENTRY,
|
||||
{
|
||||
config_entries: [SENSECAP_ENTRY, TTN_ENTRY],
|
||||
identifiers: [["sensecap_lorawan", `${TTN_ENTRY}:0000000000000002`]],
|
||||
}
|
||||
),
|
||||
],
|
||||
entityRegistryEntries: REGISTRY_ENTRIES,
|
||||
entities: () =>
|
||||
withRegistryLinks(REGISTRY_ENTRIES, {
|
||||
"sensor.lorawan_greenhouse_temperature": {
|
||||
entity_id: "sensor.lorawan_greenhouse_temperature",
|
||||
state: "24.3",
|
||||
attributes: {
|
||||
friendly_name: "Greenhouse temperature",
|
||||
device_class: "temperature",
|
||||
state_class: "measurement",
|
||||
unit_of_measurement: "°C",
|
||||
},
|
||||
},
|
||||
"sensor.lorawan_greenhouse_humidity": {
|
||||
entity_id: "sensor.lorawan_greenhouse_humidity",
|
||||
state: "58",
|
||||
attributes: {
|
||||
friendly_name: "Greenhouse humidity",
|
||||
device_class: "humidity",
|
||||
state_class: "measurement",
|
||||
unit_of_measurement: "%",
|
||||
},
|
||||
},
|
||||
"sensor.lorawan_study_carbon_dioxide": {
|
||||
entity_id: "sensor.lorawan_study_carbon_dioxide",
|
||||
state: "652",
|
||||
attributes: {
|
||||
friendly_name: "Study carbon dioxide",
|
||||
device_class: "carbon_dioxide",
|
||||
state_class: "measurement",
|
||||
unit_of_measurement: "ppm",
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -1,124 +0,0 @@
|
||||
import type { LoRaWANDevice } from "../../../../../src/data/lorawan";
|
||||
import type { MockHomeAssistant } from "../../../../../src/fake_data/provide_hass";
|
||||
import { minutesAgo } from "../helpers";
|
||||
import { emitInitial } from "../subscription";
|
||||
import { CHIRPSTACK_ENTRY, TTN_ENTRY } from "./fixtures";
|
||||
|
||||
const devices = (): LoRaWANDevice[] => {
|
||||
const base: LoRaWANDevice = {
|
||||
dev_eui: "0000000000000001",
|
||||
name: "Greenhouse",
|
||||
manufacturer: "Seeed Studio",
|
||||
model: "S2101",
|
||||
application_id: "home-sensors",
|
||||
device_id: "lorawan-greenhouse",
|
||||
connection: {
|
||||
entry_id: CHIRPSTACK_ENTRY,
|
||||
domain: "chirpstack",
|
||||
title: "Home ChirpStack",
|
||||
configuration_url: "https://www.chirpstack.io/",
|
||||
},
|
||||
support: "supported",
|
||||
last_uplink: minutesAgo(2),
|
||||
last_join: minutesAgo(2400),
|
||||
last_status: minutesAgo(64),
|
||||
receptions: [{ gateway_id: "home-indoor", rssi: -67, snr: 9.5 }],
|
||||
battery_level: 87,
|
||||
external_power_source: false,
|
||||
downlink_margin: 12,
|
||||
};
|
||||
return [
|
||||
base,
|
||||
{
|
||||
...base,
|
||||
dev_eui: "0000000000000002",
|
||||
name: "Study air quality",
|
||||
model: "S2103",
|
||||
device_id: "lorawan-study",
|
||||
application_id: "demo-home",
|
||||
connection: {
|
||||
entry_id: TTN_ENTRY,
|
||||
domain: "thethingsnetwork_lorawan",
|
||||
title: "The Things Network",
|
||||
configuration_url: "https://eu1.cloud.thethings.network/console",
|
||||
},
|
||||
receptions: [
|
||||
{ gateway_id: "brooklyn-rooftop", rssi: -103, snr: -2.5 },
|
||||
{ gateway_id: "brooklyn-community", rssi: -110, snr: -5.2 },
|
||||
],
|
||||
battery_level: 64,
|
||||
last_uplink: minutesAgo(4),
|
||||
last_status: minutesAgo(140),
|
||||
},
|
||||
{
|
||||
...base,
|
||||
dev_eui: "24E124128E123456",
|
||||
name: "Irrigation controller",
|
||||
manufacturer: "Milesight",
|
||||
model: "UC511",
|
||||
device_id: null,
|
||||
support: "integration_not_configured",
|
||||
battery_level: 93,
|
||||
last_uplink: minutesAgo(7),
|
||||
receptions: [{ gateway_id: "home-indoor", rssi: -83, snr: 6.8 }],
|
||||
},
|
||||
{
|
||||
...base,
|
||||
dev_eui: "A84041FFFF123456",
|
||||
name: "Garage power meter",
|
||||
manufacturer: "",
|
||||
model: "",
|
||||
device_id: null,
|
||||
support: "missing_identity",
|
||||
battery_level: null,
|
||||
external_power_source: true,
|
||||
last_uplink: minutesAgo(1),
|
||||
receptions: [{ gateway_id: "home-indoor", rssi: -75, snr: 8.1 }],
|
||||
},
|
||||
{
|
||||
...base,
|
||||
dev_eui: "2CF7F1C071500199",
|
||||
name: "New soil sensor",
|
||||
model: "S2105",
|
||||
device_id: null,
|
||||
support: "unsupported_model",
|
||||
last_uplink: null,
|
||||
last_join: null,
|
||||
last_status: null,
|
||||
receptions: [],
|
||||
battery_level: null,
|
||||
external_power_source: null,
|
||||
downlink_margin: null,
|
||||
},
|
||||
{
|
||||
...base,
|
||||
dev_eui: "2CF7F1C071500166",
|
||||
name: "Garden shed",
|
||||
device_id: null,
|
||||
support: "integration_not_configured",
|
||||
battery_level: 18,
|
||||
last_uplink: minutesAgo(180),
|
||||
last_status: minutesAgo(600),
|
||||
receptions: [{ gateway_id: "home-indoor", rssi: -112, snr: -7.5 }],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const mockLoRaWAN = (hass: MockHomeAssistant) => {
|
||||
const deviceList = devices();
|
||||
const snapshot = {
|
||||
devices: deviceList,
|
||||
connections: [
|
||||
...new Map(
|
||||
deviceList.map((device) => [
|
||||
device.connection.entry_id,
|
||||
device.connection,
|
||||
])
|
||||
).values(),
|
||||
],
|
||||
};
|
||||
hass.mockWS("lorawan/devices/list", () => snapshot);
|
||||
hass.mockWS("lorawan/devices/subscribe", (_msg, _hass, onChange) =>
|
||||
emitInitial(() => onChange?.(snapshot))
|
||||
);
|
||||
};
|
||||
@@ -190,39 +190,6 @@ export const removeEntityRegistryEntry = (
|
||||
hass.mockEvent("entity_registry_updated");
|
||||
};
|
||||
|
||||
export const removeLabelFromEntityRegistry = (
|
||||
hass: MockHomeAssistant,
|
||||
labelId: string
|
||||
) => {
|
||||
const withoutLabel = (labels: string[]) =>
|
||||
labels.filter((id) => id !== labelId);
|
||||
for (const [key, entry] of retiredEntries) {
|
||||
if (entry.labels.includes(labelId)) {
|
||||
retiredEntries.set(key, { ...entry, labels: withoutLabel(entry.labels) });
|
||||
}
|
||||
}
|
||||
if (!entries.some((entry) => entry.labels.includes(labelId))) {
|
||||
return;
|
||||
}
|
||||
entries = entries.map((entry) =>
|
||||
entry.labels.includes(labelId)
|
||||
? { ...entry, labels: withoutLabel(entry.labels) }
|
||||
: entry
|
||||
);
|
||||
const entities = { ...hass.entities };
|
||||
for (const entry of entries) {
|
||||
const entity = entities[entry.entity_id];
|
||||
if (entity?.labels.includes(labelId)) {
|
||||
entities[entry.entity_id] = {
|
||||
...entity,
|
||||
labels: withoutLabel(entity.labels),
|
||||
};
|
||||
}
|
||||
}
|
||||
hass.updateHass({ entities });
|
||||
hass.mockEvent("entity_registry_updated");
|
||||
};
|
||||
|
||||
// Adds an entry like core does when an integration creates an entity
|
||||
export const addEntityRegistryEntry = (
|
||||
hass: MockHomeAssistant,
|
||||
|
||||
@@ -31,7 +31,7 @@ const exposedEntities: Record<string, ExposeEntitySettings> = {
|
||||
|
||||
export const mockExpose = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("homeassistant/expose_entity/list", () => ({
|
||||
exposed_entities: structuredClone(exposedEntities),
|
||||
exposed_entities: exposedEntities,
|
||||
}));
|
||||
hass.mockWS(
|
||||
"homeassistant/expose_new_entities/get",
|
||||
@@ -39,26 +39,6 @@ export const mockExpose = (hass: MockHomeAssistant) => {
|
||||
expose_new: msg.assistant !== "cloud.google_assistant",
|
||||
})
|
||||
);
|
||||
hass.mockWS(
|
||||
"homeassistant/expose_entity",
|
||||
(msg: {
|
||||
assistants: (keyof ExposeEntitySettings)[];
|
||||
entity_ids: string[];
|
||||
should_expose: boolean;
|
||||
}) => {
|
||||
const changedEntityIds = msg.entity_ids.filter((entityId) => {
|
||||
const settings = (exposedEntities[entityId] ??= {});
|
||||
let changed = false;
|
||||
for (const assistant of msg.assistants) {
|
||||
if (Boolean(settings[assistant]) !== msg.should_expose) {
|
||||
changed = true;
|
||||
}
|
||||
settings[assistant] = msg.should_expose;
|
||||
}
|
||||
return changed;
|
||||
});
|
||||
return { changed_entity_ids: changedEntityIds };
|
||||
}
|
||||
);
|
||||
hass.mockWS("homeassistant/expose_entity", () => null);
|
||||
hass.mockWS("homeassistant/expose_new_entities/set", () => null);
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { LabelRegistryEntry } from "../../../src/data/label/label_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
import { removeLabelFromEntityRegistry } from "./entity_registry";
|
||||
|
||||
let labels: LabelRegistryEntry[] = [];
|
||||
|
||||
@@ -10,22 +9,6 @@ export const mockLabelRegistry = (
|
||||
hass: MockHomeAssistant,
|
||||
data: LabelRegistryEntry[] = []
|
||||
) => {
|
||||
labels = [...data];
|
||||
hass.mockWS("config/label_registry/list", () =>
|
||||
labels.map((label) => ({ ...label }))
|
||||
);
|
||||
hass.mockWS(
|
||||
"config/label_registry/delete",
|
||||
(msg: { label_id: string }, currentHass: MockHomeAssistant) => {
|
||||
const index = labels.findIndex(
|
||||
(label) => label.label_id === msg.label_id
|
||||
);
|
||||
if (index === -1) {
|
||||
throw new Error(`Label ${msg.label_id} not found`);
|
||||
}
|
||||
labels.splice(index, 1);
|
||||
removeLabelFromEntityRegistry(currentHass, msg.label_id);
|
||||
hass.mockEvent("label_registry_updated");
|
||||
}
|
||||
);
|
||||
labels = data;
|
||||
hass.mockWS("config/label_registry/list", () => data);
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { LocalizeFunc } from "../../../src/common/translations/localize";
|
||||
import type { LovelaceDashboard } from "../../../src/data/lovelace/dashboard";
|
||||
import type { LovelaceInfo } from "../../../src/data/lovelace/resource";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
import {
|
||||
@@ -12,14 +11,10 @@ import "../custom-cards/ha-demo-card";
|
||||
import "../custom-cards/ha-demo-next-card";
|
||||
import { mockMap } from "./map";
|
||||
|
||||
let dashboards: LovelaceDashboard[] = [];
|
||||
|
||||
export const mockLovelace = (
|
||||
hass: MockHomeAssistant,
|
||||
localizePromise: Promise<LocalizeFunc>,
|
||||
data: LovelaceDashboard[] = []
|
||||
localizePromise: Promise<LocalizeFunc>
|
||||
) => {
|
||||
dashboards = data;
|
||||
hass.mockWS("lovelace/config", ({ url_path }) => {
|
||||
if (url_path === "map") {
|
||||
mockMap(hass);
|
||||
@@ -39,16 +34,7 @@ export const mockLovelace = (
|
||||
);
|
||||
hass.mockWS("lovelace/config/save", () => Promise.resolve());
|
||||
hass.mockWS("lovelace/resources", () => Promise.resolve([]));
|
||||
hass.mockWS("lovelace/dashboards/list", () =>
|
||||
dashboards.map((dashboard) => ({ ...dashboard }))
|
||||
);
|
||||
hass.mockWS("lovelace/dashboards/delete", (msg: { dashboard_id: string }) => {
|
||||
const index = dashboards.findIndex((d) => d.id === msg.dashboard_id);
|
||||
if (index === -1) {
|
||||
throw new Error(`Dashboard ${msg.dashboard_id} not found`);
|
||||
}
|
||||
dashboards.splice(index, 1);
|
||||
});
|
||||
hass.mockWS("lovelace/dashboards/list", () => Promise.resolve([]));
|
||||
};
|
||||
|
||||
customElements.whenDefined("hui-root").then(() => {
|
||||
|
||||
@@ -221,14 +221,7 @@ const personEntity = (person: DemoPerson, now: number): EntityInput => {
|
||||
const personHistory = (person: DemoPerson, now: number): EntityHistoryState[] =>
|
||||
person.timeline.map(([hoursAgo, state]) => {
|
||||
const time = (now - hoursAgo * HOUR) / 1000;
|
||||
const zoneId =
|
||||
state === "home" ? "home" : zones.find((zone) => zone.name === state)?.id;
|
||||
return {
|
||||
s: state,
|
||||
a: { in_zones: zoneId ? [`zone.${zoneId}`] : [] },
|
||||
lu: time,
|
||||
lc: time,
|
||||
};
|
||||
return { s: state, a: {}, lu: time, lc: time };
|
||||
});
|
||||
|
||||
export const mockMap = (hass: MockHomeAssistant) => {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { TZDate } from "@date-fns/tz";
|
||||
import {
|
||||
addDays,
|
||||
addHours,
|
||||
addMinutes,
|
||||
addMonths,
|
||||
constructFrom,
|
||||
differenceInHours,
|
||||
endOfDay,
|
||||
} from "date-fns";
|
||||
@@ -15,18 +13,6 @@ import type {
|
||||
} from "../../../src/data/recorder";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
const getFirstDate = (
|
||||
start: Date,
|
||||
period: "5minute" | "hour" | "day" | "month"
|
||||
): Date => {
|
||||
if (period === "day" || period === "month") {
|
||||
return constructFrom(start, start);
|
||||
}
|
||||
// Core returns UTC-aligned buckets whose start is at or after the request.
|
||||
const interval = (period === "hour" ? 60 : 5) * 60 * 1000;
|
||||
return constructFrom(start, Math.ceil(start.getTime() / interval) * interval);
|
||||
};
|
||||
|
||||
const getNextDate = (
|
||||
currentDate: Date,
|
||||
period: "5minute" | "hour" | "day" | "month"
|
||||
@@ -47,7 +33,8 @@ const generateMeanStatistics = (
|
||||
maxDiff: number
|
||||
): StatisticValue[] => {
|
||||
const statistics: StatisticValue[] = [];
|
||||
let currentDate = getFirstDate(start, period);
|
||||
let currentDate = new Date(start);
|
||||
currentDate.setMinutes(0, 0, 0);
|
||||
const now = new Date();
|
||||
while (end > currentDate && currentDate < now) {
|
||||
const delta = Math.random() * maxDiff;
|
||||
@@ -73,17 +60,16 @@ const generateSumStatistics = (
|
||||
end: Date,
|
||||
period: "5minute" | "hour" | "day" | "month" = "hour",
|
||||
initValue: number,
|
||||
maxDiff: number | ((start: Date, end: Date) => number)
|
||||
maxDiff: number
|
||||
): StatisticValue[] => {
|
||||
const statistics: StatisticValue[] = [];
|
||||
let currentDate = getFirstDate(start, period);
|
||||
let currentDate = new Date(start);
|
||||
currentDate.setMinutes(0, 0, 0);
|
||||
let sum = initValue;
|
||||
const now = new Date();
|
||||
while (end > currentDate && currentDate < now) {
|
||||
const nextDate = getNextDate(currentDate, period);
|
||||
const bound =
|
||||
typeof maxDiff === "function" ? maxDiff(currentDate, nextDate) : maxDiff;
|
||||
const add = Math.random() * bound;
|
||||
const add = Math.random() * maxDiff;
|
||||
sum += add;
|
||||
statistics.push({
|
||||
start: currentDate.getTime(),
|
||||
@@ -110,7 +96,8 @@ const generateCurvedStatistics = (
|
||||
metered: boolean
|
||||
): StatisticValue[] => {
|
||||
const statistics: StatisticValue[] = [];
|
||||
let currentDate = getFirstDate(start, period);
|
||||
let currentDate = new Date(start);
|
||||
currentDate.setMinutes(0, 0, 0);
|
||||
let sum = initValue;
|
||||
const hours = differenceInHours(end, start) - 1;
|
||||
let i = 0;
|
||||
@@ -149,15 +136,6 @@ const statisticsFunctions: Record<
|
||||
period: "5minute" | "hour" | "day" | "month"
|
||||
) => StatisticValue[]
|
||||
> = {
|
||||
"sensor.energy_water": (_id, start, end, period = "hour") =>
|
||||
generateSumStatistics(
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
4000,
|
||||
(bucketStart, bucketEnd) =>
|
||||
(20 * (bucketEnd.getTime() - bucketStart.getTime())) / 3600000
|
||||
),
|
||||
"sensor.energy_consumption_tarif_1": (
|
||||
_id: string,
|
||||
start: Date,
|
||||
@@ -173,9 +151,9 @@ const statisticsFunctions: Record<
|
||||
period === "day" ? 17 : 504
|
||||
);
|
||||
}
|
||||
const morningEnd = addHours(start, 10);
|
||||
const morningEnd = new Date(start.getTime() + 10 * 60 * 60 * 1000);
|
||||
const morningLow = generateSumStatistics(start, morningEnd, period, 0, 0.7);
|
||||
const eveningStart = addHours(start, 20);
|
||||
const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000);
|
||||
const morningFinalVal = morningLow.length
|
||||
? morningLow[morningLow.length - 1].sum!
|
||||
: 0;
|
||||
@@ -210,8 +188,8 @@ const statisticsFunctions: Record<
|
||||
period === "day" ? 17 : 504
|
||||
);
|
||||
}
|
||||
const morningEnd = addHours(start, 9);
|
||||
const eveningStart = addHours(start, 20);
|
||||
const morningEnd = new Date(start.getTime() + 9 * 60 * 60 * 1000);
|
||||
const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000);
|
||||
const highTarif = generateSumStatistics(
|
||||
morningEnd,
|
||||
eveningStart,
|
||||
@@ -250,12 +228,9 @@ const statisticsFunctions: Record<
|
||||
period === "day" ? 17 : 504
|
||||
);
|
||||
}
|
||||
const productionStart = addHours(start, 9);
|
||||
const productionEnd = addHours(start, 21);
|
||||
const dayEnd = constructFrom(
|
||||
end,
|
||||
Math.min(endOfDay(productionEnd).getTime(), end.getTime())
|
||||
);
|
||||
const productionStart = new Date(start.getTime() + 9 * 60 * 60 * 1000);
|
||||
const productionEnd = new Date(start.getTime() + 21 * 60 * 60 * 1000);
|
||||
const dayEnd = new Date(endOfDay(productionEnd));
|
||||
const production = generateCurvedStatistics(
|
||||
productionStart,
|
||||
productionEnd,
|
||||
@@ -294,12 +269,9 @@ const statisticsFunctions: Record<
|
||||
period === "day" ? 17 : 504
|
||||
);
|
||||
}
|
||||
const productionStart = addHours(start, 7);
|
||||
const productionEnd = addHours(start, 23);
|
||||
const dayEnd = constructFrom(
|
||||
end,
|
||||
Math.min(endOfDay(productionEnd).getTime(), end.getTime())
|
||||
);
|
||||
const productionStart = new Date(start.getTime() + 7 * 60 * 60 * 1000);
|
||||
const productionEnd = new Date(start.getTime() + 23 * 60 * 60 * 1000);
|
||||
const dayEnd = new Date(endOfDay(productionEnd));
|
||||
const production = generateCurvedStatistics(
|
||||
productionStart,
|
||||
productionEnd,
|
||||
@@ -329,11 +301,6 @@ const statisticsFunctions: Record<
|
||||
return [...morning, ...production, ...evening, ...rest];
|
||||
},
|
||||
};
|
||||
// Prices are per source unit; water uses litres (4 currency units per m³).
|
||||
const derivedCosts: Record<string, { source: string; price: number }> = {
|
||||
"sensor.energy_water_cost": { source: "sensor.energy_water", price: 0.004 },
|
||||
};
|
||||
|
||||
export const mockRecorder = (mockHass: MockHomeAssistant) => {
|
||||
mockHass.mockWS(
|
||||
"recorder/get_statistics_metadata",
|
||||
@@ -346,28 +313,14 @@ export const mockRecorder = (mockHass: MockHomeAssistant) => {
|
||||
mockHass.mockWS(
|
||||
"recorder/statistics_during_period",
|
||||
({ statistic_ids, start_time, end_time, period }, hass): Statistics => {
|
||||
// Calendar buckets follow the server timezone, independent of the browser.
|
||||
const start = new TZDate(start_time, hass.config.time_zone);
|
||||
const end = new TZDate(end_time ?? Date.now(), hass.config.time_zone);
|
||||
const start = new Date(start_time);
|
||||
const end = end_time ? new Date(end_time) : new Date();
|
||||
|
||||
const statistics: Record<string, StatisticValue[]> = {};
|
||||
const cache: Record<string, StatisticValue[]> = {};
|
||||
const getStats = (id: string) => {
|
||||
cache[id] ??= statisticsFunctions[id](id, start, end, period);
|
||||
return cache[id];
|
||||
};
|
||||
|
||||
statistic_ids.forEach((id: string) => {
|
||||
if (id in derivedCosts) {
|
||||
const { source, price } = derivedCosts[id];
|
||||
let cost = 0;
|
||||
statistics[id] = getStats(source).map((statistic) => {
|
||||
const change = statistic.change! * price;
|
||||
cost += change;
|
||||
return { ...statistic, change, state: cost, sum: cost };
|
||||
});
|
||||
} else if (id in statisticsFunctions) {
|
||||
statistics[id] = getStats(id);
|
||||
if (id in statisticsFunctions) {
|
||||
statistics[id] = statisticsFunctions[id](id, start, end, period);
|
||||
} else {
|
||||
const entityState = hass.states[id];
|
||||
const state = entityState ? Number(entityState.state) : 1;
|
||||
|
||||
@@ -37,7 +37,6 @@ export default [
|
||||
pages: [
|
||||
"ha-form",
|
||||
"ha-selector",
|
||||
"ha-target-picker-exclusions",
|
||||
"ha-select-box",
|
||||
"ha-input",
|
||||
"ha-textarea",
|
||||
@@ -127,7 +126,6 @@ export default [
|
||||
"gauge-card",
|
||||
"plant-card",
|
||||
"map-card",
|
||||
"calendar-card",
|
||||
"media-control-card",
|
||||
"media-player-row",
|
||||
],
|
||||
@@ -169,10 +167,6 @@ export default [
|
||||
header: "Inputs and values",
|
||||
pages: ["input-number", "input-text", "number", "timer"],
|
||||
},
|
||||
{
|
||||
header: "Time and schedules",
|
||||
pages: ["calendar"],
|
||||
},
|
||||
{
|
||||
header: "System",
|
||||
pages: ["update"],
|
||||
|
||||
@@ -6,10 +6,12 @@ import "../../../src/components/ha-settings-row";
|
||||
import "../../../src/components/ha-switch";
|
||||
import type { HaSwitch } from "../../../src/components/ha-switch";
|
||||
import "../../../src/components/ha-theme-settings";
|
||||
import type { ThemeSettings } from "../../../src/types";
|
||||
import type { HomeAssistant, ThemeSettings } from "../../../src/types";
|
||||
|
||||
@customElement("gallery-settings")
|
||||
class GallerySettings extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public themeSettings!: ThemeSettings;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@@ -24,6 +26,7 @@ class GallerySettings extends LitElement {
|
||||
Configure how the gallery renders component previews and examples.
|
||||
</div>
|
||||
<ha-theme-settings
|
||||
.hass=${this.hass}
|
||||
.selectedTheme=${this.themeSettings}
|
||||
.narrow=${this.narrow}
|
||||
.heading=${"Theme"}
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
import { mockEntityRegistry } from "../../../demo/src/stubs/entity_registry";
|
||||
import type { CalendarEventApiData } from "../../../src/data/calendar";
|
||||
import { CalendarEntityFeature } from "../../../src/data/calendar";
|
||||
import type { EntityRegistryEntry } from "../../../src/data/entity/entity_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
// Sample calendars with events relative to today, so demos always show
|
||||
// something in the current week and month.
|
||||
|
||||
const pad = (value: number) => String(value).padStart(2, "0");
|
||||
|
||||
const day = (offset: number): Date => {
|
||||
const date = new Date();
|
||||
date.setHours(0, 0, 0, 0);
|
||||
date.setDate(date.getDate() + offset);
|
||||
return date;
|
||||
};
|
||||
|
||||
const isoDate = (date: Date) =>
|
||||
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||
|
||||
const at = (offset: number, hours: number, minutes = 0) => {
|
||||
const date = day(offset);
|
||||
date.setHours(hours, minutes);
|
||||
return date.toISOString();
|
||||
};
|
||||
|
||||
const allDay = (offset: number, days = 1) => ({
|
||||
start: isoDate(day(offset)),
|
||||
end: isoDate(day(offset + days)),
|
||||
all_day: true,
|
||||
});
|
||||
|
||||
const FAMILY_EVENTS: CalendarEventApiData[] = [
|
||||
{ summary: "School run", start: at(0, 8), end: at(0, 8, 30) },
|
||||
{
|
||||
summary: "Dentist",
|
||||
start: at(0, 15, 30),
|
||||
end: at(0, 16, 15),
|
||||
location: "Main Street 12",
|
||||
},
|
||||
{ summary: "Grandma's birthday", ...allDay(1) },
|
||||
{ summary: "Football practice", start: at(2, 17), end: at(2, 18, 30) },
|
||||
{
|
||||
summary: "Dinner with friends",
|
||||
start: at(3, 19),
|
||||
end: at(3, 22),
|
||||
status: "tentative",
|
||||
},
|
||||
{ summary: "Summer cabin", ...allDay(5, 3) },
|
||||
{ summary: "Parent-teacher meeting", start: at(9, 18), end: at(9, 19) },
|
||||
{ summary: "Football practice", start: at(9, 17), end: at(9, 18, 30) },
|
||||
{ summary: "Swimming lessons", start: at(12, 10), end: at(12, 11) },
|
||||
{ summary: "Car service", start: at(-3, 9), end: at(-3, 11) },
|
||||
{ summary: "Book club", start: at(-6, 19), end: at(-6, 21) },
|
||||
];
|
||||
|
||||
const WORK_EVENTS: CalendarEventApiData[] = [
|
||||
{ summary: "Stand-up", start: at(0, 9), end: at(0, 9, 15) },
|
||||
{ summary: "Sprint planning", start: at(2, 10), end: at(2, 12) },
|
||||
{ summary: "Team lunch", start: at(4, 12), end: at(4, 13) },
|
||||
{ summary: "Conference", ...allDay(8, 2) },
|
||||
{ summary: "Quarterly review", start: at(-2, 14), end: at(-2, 15, 30) },
|
||||
];
|
||||
|
||||
const EVENTS: Record<string, CalendarEventApiData[] | null> = {
|
||||
"calendar.family": FAMILY_EVENTS,
|
||||
"calendar.work": WORK_EVENTS,
|
||||
"calendar.empty": [],
|
||||
// A calendar whose integration fails to return events
|
||||
"calendar.holidays": null,
|
||||
};
|
||||
|
||||
export const CALENDAR_ENTITIES = [
|
||||
{
|
||||
entity_id: "calendar.family",
|
||||
state: "off",
|
||||
attributes: {
|
||||
friendly_name: "Family",
|
||||
message: "School run",
|
||||
all_day: false,
|
||||
start_time: at(0, 8),
|
||||
end_time: at(0, 8, 30),
|
||||
location: "",
|
||||
description: "",
|
||||
supported_features:
|
||||
CalendarEntityFeature.CREATE_EVENT +
|
||||
CalendarEntityFeature.DELETE_EVENT +
|
||||
CalendarEntityFeature.UPDATE_EVENT,
|
||||
},
|
||||
},
|
||||
{
|
||||
entity_id: "calendar.work",
|
||||
state: "off",
|
||||
attributes: {
|
||||
friendly_name: "Work",
|
||||
message: "Stand-up",
|
||||
all_day: false,
|
||||
start_time: at(0, 9),
|
||||
end_time: at(0, 9, 15),
|
||||
},
|
||||
},
|
||||
{
|
||||
entity_id: "calendar.empty",
|
||||
state: "off",
|
||||
attributes: { friendly_name: "Empty calendar" },
|
||||
},
|
||||
{
|
||||
entity_id: "calendar.holidays",
|
||||
state: "off",
|
||||
attributes: { friendly_name: "Holidays (failing)" },
|
||||
},
|
||||
];
|
||||
|
||||
const registryEntry = (
|
||||
entityId: string,
|
||||
color?: string
|
||||
): EntityRegistryEntry => ({
|
||||
id: entityId,
|
||||
entity_id: entityId,
|
||||
name: null,
|
||||
icon: null,
|
||||
platform: "local_calendar",
|
||||
config_entry_id: null,
|
||||
config_subentry_id: null,
|
||||
device_id: null,
|
||||
area_id: null,
|
||||
labels: [],
|
||||
disabled_by: null,
|
||||
hidden_by: null,
|
||||
entity_category: null,
|
||||
has_entity_name: false,
|
||||
unique_id: entityId,
|
||||
options: color ? { calendar: { color } } : null,
|
||||
categories: {},
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
});
|
||||
|
||||
export const mockCalendars = (hass: MockHomeAssistant) => {
|
||||
mockEntityRegistry(hass, [
|
||||
registryEntry("calendar.family"),
|
||||
// Shows a color picked in the entity settings
|
||||
registryEntry("calendar.work", "deep-orange"),
|
||||
registryEntry("calendar.empty"),
|
||||
registryEntry("calendar.holidays"),
|
||||
]);
|
||||
hass.mockWS("calendar/event/subscribe", (msg, _hass, onChange) => {
|
||||
const start = new Date(msg.start).getTime();
|
||||
const end = new Date(msg.end).getTime();
|
||||
const events = EVENTS[msg.entity_id];
|
||||
onChange?.({
|
||||
events:
|
||||
events?.filter(
|
||||
(event) =>
|
||||
new Date(event.end as string).getTime() > start &&
|
||||
new Date(event.start as string).getTime() < end
|
||||
) ?? null,
|
||||
});
|
||||
return () => undefined;
|
||||
});
|
||||
// Changes made in the event dialogs are accepted but not stored
|
||||
hass.mockWS("calendar/event/create", () => null);
|
||||
hass.mockWS("calendar/event/update", () => null);
|
||||
hass.mockWS("calendar/event/delete", () => null);
|
||||
};
|
||||
@@ -151,16 +151,6 @@ export const createMediaPlayerEntities = () => [
|
||||
supported_features: 289,
|
||||
},
|
||||
},
|
||||
{
|
||||
entity_id: "media_player.theater_off_assumed",
|
||||
state: "off",
|
||||
attributes: {
|
||||
friendly_name: "TV Off (assumed state, cannot be switched on)",
|
||||
assumed_state: true,
|
||||
// Off + Next + Pause
|
||||
supported_features: 289,
|
||||
},
|
||||
},
|
||||
{
|
||||
entity_id: "media_player.theater_on_static",
|
||||
state: "on",
|
||||
|
||||
@@ -242,6 +242,7 @@ class HaGallery extends LitElement {
|
||||
${
|
||||
isSettingsPage
|
||||
? html`<gallery-settings
|
||||
.hass=${this._galleryHass}
|
||||
.themeSettings=${this._themeSettings}
|
||||
.narrow=${this._narrow}
|
||||
.rtl=${this._rtl}
|
||||
|
||||
@@ -12,11 +12,6 @@
|
||||
<script type="module" src="<%= entry %>"></script>
|
||||
<% } %>
|
||||
<style>
|
||||
html {
|
||||
/* Prevent iOS Safari from inflating text when rotating to landscape */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
body {
|
||||
font-family: Roboto, Noto, sans-serif;
|
||||
margin: 0;
|
||||
|
||||
@@ -35,12 +35,6 @@ const ENTITIES = [
|
||||
|
||||
const triggers = [
|
||||
{ trigger: "state", entity_id: "light.kitchen", from: "off", to: "on" },
|
||||
{
|
||||
trigger: "state",
|
||||
entity_id: "light.kitchen",
|
||||
not_from: ["unavailable", "unknown"],
|
||||
not_to: "off",
|
||||
},
|
||||
{ trigger: "mqtt" },
|
||||
{
|
||||
trigger: "geo_location",
|
||||
|
||||
@@ -33,15 +33,7 @@ import "../../components/demo-black-white-row";
|
||||
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
||||
{
|
||||
name: "State",
|
||||
triggers: [
|
||||
{ ...HaStateTrigger.defaultConfig },
|
||||
{
|
||||
trigger: "state",
|
||||
entity_id: "light.kitchen",
|
||||
not_from: ["unavailable", "unknown"],
|
||||
to: "on",
|
||||
},
|
||||
],
|
||||
triggers: [{ ...HaStateTrigger.defaultConfig }],
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ import "../../../../src/components/ha-form/ha-form";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
|
||||
const SCHEMA: HaFormSchema[] = [
|
||||
{ type: "string", name: "Name", default: "", autofocus: true },
|
||||
@@ -30,8 +31,11 @@ type DialogType =
|
||||
export class DemoHaAdaptiveDialog extends LitElement {
|
||||
@state() private _openDialog: DialogType = false;
|
||||
|
||||
@state() private _hass?: HomeAssistant;
|
||||
|
||||
protected firstUpdated() {
|
||||
provideHass(this);
|
||||
const hass = provideHass(this);
|
||||
this._hass = hass;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -85,6 +89,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
</ha-card>
|
||||
|
||||
<ha-adaptive-dialog
|
||||
.hass=${this._hass}
|
||||
.open=${this._openDialog === "basic"}
|
||||
header-title="Basic adaptive dialog"
|
||||
@closed=${this._handleClosed}
|
||||
@@ -93,6 +98,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
</ha-adaptive-dialog>
|
||||
|
||||
<ha-adaptive-dialog
|
||||
.hass=${this._hass}
|
||||
.open=${this._openDialog === "basic-subtitle-below"}
|
||||
header-title="Adaptive dialog with subtitle"
|
||||
header-subtitle="This is an adaptive dialog with a subtitle below"
|
||||
@@ -102,6 +108,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
</ha-adaptive-dialog>
|
||||
|
||||
<ha-adaptive-dialog
|
||||
.hass=${this._hass}
|
||||
.open=${this._openDialog === "basic-subtitle-above"}
|
||||
header-title="Adaptive dialog with subtitle above"
|
||||
header-subtitle="This is an adaptive dialog with a subtitle above"
|
||||
@@ -112,6 +119,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
</ha-adaptive-dialog>
|
||||
|
||||
<ha-adaptive-dialog
|
||||
.hass=${this._hass}
|
||||
.open=${this._openDialog === "small"}
|
||||
width="small"
|
||||
header-title="Small adaptive dialog"
|
||||
@@ -121,6 +129,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
</ha-adaptive-dialog>
|
||||
|
||||
<ha-adaptive-dialog
|
||||
.hass=${this._hass}
|
||||
.open=${this._openDialog === "large"}
|
||||
width="large"
|
||||
header-title="Large adaptive dialog"
|
||||
@@ -130,6 +139,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
</ha-adaptive-dialog>
|
||||
|
||||
<ha-adaptive-dialog
|
||||
.hass=${this._hass}
|
||||
.open=${this._openDialog === "form"}
|
||||
header-title="Adaptive dialog with form"
|
||||
header-subtitle="This is an adaptive dialog with a form"
|
||||
@@ -153,6 +163,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
</ha-adaptive-dialog>
|
||||
|
||||
<ha-adaptive-dialog
|
||||
.hass=${this._hass}
|
||||
.allowModeChange=${this._openDialog === "allow-mode-change"}
|
||||
header-title="Adaptive dialog with allow mode change"
|
||||
header-subtitle="Resize the window while this dialog is open"
|
||||
@@ -165,6 +176,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
</ha-adaptive-dialog>
|
||||
|
||||
<ha-adaptive-dialog
|
||||
.hass=${this._hass}
|
||||
.open=${this._openDialog === "actions"}
|
||||
header-title="Adaptive dialog with actions"
|
||||
header-subtitle="This is an adaptive dialog with header actions"
|
||||
@@ -383,6 +395,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
<h3>Example usage</h3>
|
||||
|
||||
<pre><code><ha-adaptive-dialog
|
||||
.hass=\${this.hass}
|
||||
open
|
||||
header-title="Dialog title"
|
||||
header-subtitle="Dialog subtitle"
|
||||
@@ -594,7 +607,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
|
||||
<p>Example:</p>
|
||||
|
||||
<pre><code><ha-adaptive-dialog open>
|
||||
<pre><code><ha-adaptive-dialog .hass=\${this.hass} open>
|
||||
<ha-form autofocus .schema=\${schema}></ha-form>
|
||||
</ha-adaptive-dialog></code></pre>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import "../../../../src/components/ha-form/ha-form";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
|
||||
const SCHEMA: HaFormSchema[] = [
|
||||
@@ -22,10 +23,12 @@ type PopoverType = false | "basic" | "small" | "large" | "form" | "actions";
|
||||
export class DemoHaAdaptivePopover extends LitElement {
|
||||
@state() private _openPopover: PopoverType = false;
|
||||
|
||||
@state() private _hass?: HomeAssistant;
|
||||
|
||||
@state() private _dialogAnchor?: HTMLElement;
|
||||
|
||||
protected firstUpdated() {
|
||||
provideHass(this);
|
||||
this._hass = provideHass(this);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -69,6 +72,7 @@ export class DemoHaAdaptivePopover extends LitElement {
|
||||
</ha-card>
|
||||
|
||||
<ha-adaptive-popover
|
||||
.hass=${this._hass}
|
||||
.open=${this._openPopover === "basic"}
|
||||
.dialogAnchor=${this._dialogAnchor}
|
||||
header-title="Basic adaptive popover"
|
||||
@@ -82,6 +86,7 @@ export class DemoHaAdaptivePopover extends LitElement {
|
||||
</ha-adaptive-popover>
|
||||
|
||||
<ha-adaptive-popover
|
||||
.hass=${this._hass}
|
||||
.open=${this._openPopover === "small"}
|
||||
.dialogAnchor=${this._dialogAnchor}
|
||||
width="small"
|
||||
@@ -92,6 +97,7 @@ export class DemoHaAdaptivePopover extends LitElement {
|
||||
</ha-adaptive-popover>
|
||||
|
||||
<ha-adaptive-popover
|
||||
.hass=${this._hass}
|
||||
.open=${this._openPopover === "large"}
|
||||
.dialogAnchor=${this._dialogAnchor}
|
||||
width="large"
|
||||
@@ -102,6 +108,7 @@ export class DemoHaAdaptivePopover extends LitElement {
|
||||
</ha-adaptive-popover>
|
||||
|
||||
<ha-adaptive-popover
|
||||
.hass=${this._hass}
|
||||
.open=${this._openPopover === "form"}
|
||||
.dialogAnchor=${this._dialogAnchor}
|
||||
header-title="Adaptive popover with form"
|
||||
@@ -126,6 +133,7 @@ export class DemoHaAdaptivePopover extends LitElement {
|
||||
</ha-adaptive-popover>
|
||||
|
||||
<ha-adaptive-popover
|
||||
.hass=${this._hass}
|
||||
.open=${this._openPopover === "actions"}
|
||||
.dialogAnchor=${this._dialogAnchor}
|
||||
header-title="Adaptive popover with actions"
|
||||
|
||||
@@ -51,12 +51,11 @@ The badge component is a small component that displays a number or status inform
|
||||
|
||||
**Properties/Attributes**
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --------------- | ----------------------- | ----------- | ------------------------------------------------------------- |
|
||||
| type | `"badge"` or `"button"` | `"badge"` | If it's button it shows a ripple effect |
|
||||
| label | string | `undefined` | Text label for the badge, only visible if `iconOnly = false` |
|
||||
| iconOnly | boolean | `false` | Only show label |
|
||||
| accessibleLabel | string | `undefined` | Accessible name of the button, only used if `iconOnly = true` |
|
||||
| Name | Type | Default | Description |
|
||||
| -------- | ----------------------- | ----------- | ------------------------------------------------------------ |
|
||||
| type | `"badge"` or `"button"` | `"badge"` | If it's button it shows a ripple effect |
|
||||
| label | string | `undefined` | Text label for the badge, only visible if `iconOnly = false` |
|
||||
| iconOnly | boolean | `false` | Only show label |
|
||||
|
||||
**CSS Custom Properties**
|
||||
|
||||
|
||||
@@ -63,25 +63,26 @@ means supplying one.
|
||||
|
||||
### Properties/Attributes
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ------------------ | ------------------ | --------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| getItems | Function | - | Required. Returns the items, optionally filtered by search string and section. |
|
||||
| getAdditionalItems | Function | - | Extra items appended to search results, for example "add new". |
|
||||
| value | String | - | Id of the current item. Marks its row `selected`, and the virtualized list opens with that row in view. |
|
||||
| label | String | - | Placeholder for the search field. Falls back to a localized "Search". |
|
||||
| mode | "popover"/"dialog" | "popover" | Adjusts padding for the surface the list sits in. |
|
||||
| shown | Boolean | true | Whether the surface finished animating. `ha-generic-picker` sets it so the virtualizer does not measure rows mid-animation. |
|
||||
| sections | Array | - | Filter chips. Section headers are plain strings returned by `getItems`, and the sticky title shows the one above the first visible row. |
|
||||
| selectedSection | String | - | Section filter selected when the list is initialized. |
|
||||
| searchKeys | Array | - | Fuse weighted keys for fuzzy search. |
|
||||
| searchFn | Function | - | Post-processes filtered results. |
|
||||
| rowRenderer | Function | - | Replaces the default row template. |
|
||||
| allowCustomValue | Boolean | false | Offers the typed string as an item. |
|
||||
| customValueLabel | String | - | Label for that custom item. |
|
||||
| notFoundLabel | String/Function | - | Shown when a search matches nothing. |
|
||||
| emptyLabel | String | - | Shown when there are no items at all. |
|
||||
| noSort | Boolean | false | Keeps the order `getItems` returned. |
|
||||
| clearable | Boolean | false | Adjusts search field padding for a clear affordance. |
|
||||
| Name | Type | Default | Description |
|
||||
| -------------------- | ------------------ | --------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| getItems | Function | - | Required. Returns the items, optionally filtered by search string and section. |
|
||||
| getAdditionalItems | Function | - | Extra items appended to search results, for example "add new". |
|
||||
| value | String | - | Id of the current item. Marks its row `selected`, and the virtualized list opens with that row in view. |
|
||||
| label | String | - | Placeholder for the search field. Falls back to a localized "Search". |
|
||||
| mode | "popover"/"dialog" | "popover" | Adjusts padding for the surface the list sits in. |
|
||||
| shown | Boolean | true | Whether the surface finished animating. `ha-generic-picker` sets it so the virtualizer does not measure rows mid-animation. |
|
||||
| sections | Array | - | Filter chips. Section headers are plain strings returned by `getItems`. |
|
||||
| selectedSection | String | - | Section filter selected when the list is initialized. |
|
||||
| sectionTitleFunction | Function | - | Builds the sticky section title from the visible range. |
|
||||
| searchKeys | Array | - | Fuse weighted keys for fuzzy search. |
|
||||
| searchFn | Function | - | Post-processes filtered results. |
|
||||
| rowRenderer | Function | - | Replaces the default row template. |
|
||||
| allowCustomValue | Boolean | false | Offers the typed string as an item. |
|
||||
| customValueLabel | String | - | Label for that custom item. |
|
||||
| notFoundLabel | String/Function | - | Shown when a search matches nothing. |
|
||||
| emptyLabel | String | - | Shown when there are no items at all. |
|
||||
| noSort | Boolean | false | Keeps the order `getItems` returned. |
|
||||
| clearable | Boolean | false | Adjusts search field padding for a clear affordance. |
|
||||
|
||||
### Events
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
title: Target picker exclusions
|
||||
subtitle: How the target picker shows and edits the exclude of a target
|
||||
---
|
||||
|
||||
A target can leave things out again with `exclude`, which has the same shape as
|
||||
the target itself. Excludes are global: an exclusion takes its entities out of
|
||||
every included target, not only one.
|
||||
|
||||
The target selector shows exclusions as follows:
|
||||
|
||||
- **Exclude** sits next to **Add target**. It offers only what overlaps with the
|
||||
included targets and hides what is already excluded. Child devices of an
|
||||
included device can still be excluded on their own.
|
||||
- Each included target lists, muted, the exclusions that take entities out of
|
||||
it, with how many they remove. Its count says how many are excluded.
|
||||
- An exclusion that takes nothing out of any target, like one of a deleted
|
||||
area, is listed under **Exclusions with no effect**, where it can be removed.
|
||||
- The target details show which entities are excluded and by what, and let
|
||||
you tick or untick what the target holds.
|
||||
|
||||
Only the target selector offers **Exclude**. Other users of the target picker
|
||||
set `allow-exclude` once they handle `exclude`.
|
||||
@@ -1,302 +0,0 @@
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { mockConfigEntries } from "../../../../demo/src/stubs/config_entries";
|
||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import { ensureArray } from "../../../../src/common/array/ensure-array";
|
||||
import type { HASSDomEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-selector/ha-selector";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import type { EntityRegistryDisplayEntry } from "../../../../src/data/entity/entity_registry";
|
||||
import type { Selector } from "../../../../src/data/selector";
|
||||
import {
|
||||
showDialog,
|
||||
type ShowDialogParams,
|
||||
} from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import type { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/demo-black-white-row";
|
||||
|
||||
const device = (
|
||||
id: string,
|
||||
name: string,
|
||||
areaId: string,
|
||||
parentDeviceId: string | null = null
|
||||
): DeviceRegistryEntry => ({
|
||||
area_id: areaId,
|
||||
configuration_url: null,
|
||||
config_entries: [],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
id,
|
||||
identifiers: [["demo", id] as [string, string]],
|
||||
manufacturer: null,
|
||||
model: null,
|
||||
model_id: null,
|
||||
name_by_user: null,
|
||||
name,
|
||||
sw_version: null,
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
primary_config_entry: null,
|
||||
parent_device_id: parentDeviceId,
|
||||
});
|
||||
|
||||
// A power strip in the backyard with two outlets as child devices, and a lamp
|
||||
// in the bedroom.
|
||||
const DEVICES: DeviceRegistryEntry[] = [
|
||||
device("power_strip", "Power strip", "backyard"),
|
||||
device("outlet_1", "Outlet 1", "backyard", "power_strip"),
|
||||
device("outlet_2", "Outlet 2", "backyard", "power_strip"),
|
||||
device("bedroom_lamp", "Bedroom lamp", "bedroom"),
|
||||
];
|
||||
|
||||
const ENTITY_REGISTRY: Record<string, EntityRegistryDisplayEntry> = {
|
||||
"switch.outlet_1": {
|
||||
entity_id: "switch.outlet_1",
|
||||
name: "Outlet 1",
|
||||
device_id: "outlet_1",
|
||||
platform: "demo",
|
||||
labels: [],
|
||||
},
|
||||
"switch.outlet_2": {
|
||||
entity_id: "switch.outlet_2",
|
||||
name: "Outlet 2",
|
||||
device_id: "outlet_2",
|
||||
platform: "demo",
|
||||
labels: [],
|
||||
},
|
||||
"light.bedroom_lamp": {
|
||||
entity_id: "light.bedroom_lamp",
|
||||
name: "Bedroom lamp",
|
||||
device_id: "bedroom_lamp",
|
||||
platform: "demo",
|
||||
labels: [],
|
||||
},
|
||||
"light.bedside": {
|
||||
entity_id: "light.bedside",
|
||||
name: "Bedside light",
|
||||
area_id: "bedroom",
|
||||
platform: "demo",
|
||||
labels: [],
|
||||
},
|
||||
"light.night_light": {
|
||||
entity_id: "light.night_light",
|
||||
name: "Night light",
|
||||
area_id: "bedroom",
|
||||
platform: "demo",
|
||||
labels: [],
|
||||
},
|
||||
};
|
||||
|
||||
const ENTITIES = Object.values(ENTITY_REGISTRY).map((entity) => ({
|
||||
entity_id: entity.entity_id,
|
||||
state: entity.entity_id.startsWith("light.") ? "on" : "off",
|
||||
attributes: { friendly_name: entity.name },
|
||||
}));
|
||||
|
||||
const area = (areaId: string, name: string): AreaRegistryEntry => ({
|
||||
area_id: areaId,
|
||||
floor_id: null,
|
||||
name,
|
||||
icon: null,
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
temperature_entity_id: null,
|
||||
humidity_entity_id: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
});
|
||||
|
||||
const AREAS: AreaRegistryEntry[] = [
|
||||
area("backyard", "Backyard"),
|
||||
area("bedroom", "Bedroom"),
|
||||
];
|
||||
|
||||
// Resolves a target against the demo registries, like core's
|
||||
// extract_from_target does: a device includes its child devices, and an area
|
||||
// its devices and entities.
|
||||
const extractFromTarget = ({ target }: { target: HassServiceTarget }) => {
|
||||
const ids = (key: keyof HassServiceTarget): string[] =>
|
||||
target[key] ? ensureArray(target[key]) : [];
|
||||
const areaIds = ids("area_id").filter((id) =>
|
||||
AREAS.some((a) => a.area_id === id)
|
||||
);
|
||||
const deviceIds = new Set([
|
||||
...ids("device_id").filter((id) => DEVICES.some((d) => d.id === id)),
|
||||
...DEVICES.filter((d) => d.area_id && areaIds.includes(d.area_id)).map(
|
||||
(d) => d.id
|
||||
),
|
||||
]);
|
||||
DEVICES.forEach((d) => {
|
||||
if (d.parent_device_id && deviceIds.has(d.parent_device_id)) {
|
||||
deviceIds.add(d.id);
|
||||
}
|
||||
});
|
||||
const entityIds = new Set(ids("entity_id"));
|
||||
Object.values(ENTITY_REGISTRY).forEach((entity) => {
|
||||
if (
|
||||
(entity.device_id && deviceIds.has(entity.device_id)) ||
|
||||
(entity.area_id && areaIds.includes(entity.area_id))
|
||||
) {
|
||||
entityIds.add(entity.entity_id);
|
||||
}
|
||||
});
|
||||
return {
|
||||
referenced_entities: [...entityIds],
|
||||
referenced_devices: [...deviceIds],
|
||||
referenced_areas: areaIds,
|
||||
missing_areas: [],
|
||||
missing_devices: [],
|
||||
missing_floors: [],
|
||||
missing_labels: [],
|
||||
};
|
||||
};
|
||||
|
||||
interface Sample {
|
||||
name: string;
|
||||
description: string;
|
||||
selector: Selector;
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
const SAMPLES: Sample[] = [
|
||||
{
|
||||
name: "Target with exclusions",
|
||||
description:
|
||||
"Each target lists what is excluded from it. An exclusion of an area that no longer exists has no effect and is listed apart.",
|
||||
selector: { target: {} },
|
||||
value: {
|
||||
area_id: ["bedroom", "backyard"],
|
||||
exclude: {
|
||||
entity_id: ["light.night_light"],
|
||||
device_id: ["outlet_2"],
|
||||
area_id: ["deleted_area"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Target without exclusions",
|
||||
description:
|
||||
"Exclude offers what the power strip includes, so its outlets can be excluded on their own.",
|
||||
selector: { target: {} },
|
||||
value: { device_id: ["power_strip"] },
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-components-ha-target-picker-exclusions")
|
||||
class DemoHaTargetPickerExclusions
|
||||
extends LitElement
|
||||
implements ProvideHassElement
|
||||
{
|
||||
@state() public hass!: HomeAssistant;
|
||||
|
||||
@state() private _values = SAMPLES.map((sample) => sample.value);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("config", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
mockEntityRegistry(hass);
|
||||
mockDeviceRegistry(hass, DEVICES);
|
||||
mockConfigEntries(hass);
|
||||
mockHassioSupervisor(hass);
|
||||
// Set the areas and entity registry directly, typed against the real
|
||||
// registry types, instead of the demo stubs whose types differ.
|
||||
const areas: Record<string, AreaRegistryEntry> = {};
|
||||
AREAS.forEach((a) => {
|
||||
areas[a.area_id] = a;
|
||||
});
|
||||
hass.updateHass({ areas, entities: ENTITY_REGISTRY });
|
||||
hass.mockWS("config/device_registry/list_composite_splits", () => ({}));
|
||||
hass.mockWS("extract_from_target", extractFromTarget);
|
||||
hass.mockWS("auth/sign_path", (params) => params);
|
||||
}
|
||||
|
||||
public provideHass(el) {
|
||||
el.hass = this.hass;
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("show-dialog", this._dialogManager);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("show-dialog", this._dialogManager);
|
||||
}
|
||||
|
||||
private _dialogManager = (e: HASSDomEvent<ShowDialogParams<unknown>>) => {
|
||||
const { dialogTag, dialogImport, dialogParams, addHistory, parentElement } =
|
||||
e.detail;
|
||||
showDialog(
|
||||
this,
|
||||
dialogTag,
|
||||
dialogParams,
|
||||
dialogImport,
|
||||
parentElement,
|
||||
addHistory
|
||||
);
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${SAMPLES.map(
|
||||
(sample, idx) => html`
|
||||
<demo-black-white-row .title=${sample.name}>
|
||||
${["light", "dark"].map(
|
||||
(slot) => html`
|
||||
<ha-settings-row narrow slot=${slot}>
|
||||
<span slot="heading">${sample.name}</span>
|
||||
<span slot="description">${sample.description}</span>
|
||||
<ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${sample.selector}
|
||||
.value=${this._values[idx]}
|
||||
.sampleIdx=${idx}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
></ha-selector>
|
||||
</ha-settings-row>
|
||||
`
|
||||
)}
|
||||
</demo-black-white-row>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleValueChanged(ev) {
|
||||
const idx = ev.target.sampleIdx;
|
||||
this._values = this._values.map((value, i) =>
|
||||
i === idx ? ev.detail.value : value
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-settings-row {
|
||||
--settings-row-content-width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-target-picker-exclusions": DemoHaTargetPickerExclusions;
|
||||
}
|
||||
}
|
||||
@@ -33,4 +33,4 @@ We want to make it as easy for designers to contribute as it is for developers.
|
||||
|
||||
## Developers
|
||||
|
||||
Everything you need to get started developing can be found in our <a href="https://contributors.home-assistant.io/frontend/" rel="noopener noreferrer" target="_blank">Home Assistant contributor docs</a>.
|
||||
Everything you need to get started developing can be found in our <a href="https://developers.home-assistant.io" rel="noopener noreferrer" target="_blank">Home Assistant Developer Docs</a>.
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
title: Calendar card
|
||||
---
|
||||
@@ -1,114 +0,0 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { ShowDialogParams } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import type { CalendarCardConfig } from "../../../../src/panels/lovelace/cards/types";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import type { DemoCardConfig } from "../../components/demo-card";
|
||||
import "../../components/demo-cards";
|
||||
import { CALENDAR_ENTITIES, mockCalendars } from "../../data/calendar";
|
||||
|
||||
const CONFIGS = [
|
||||
{
|
||||
heading: "Month view",
|
||||
config: {
|
||||
type: "calendar",
|
||||
entities: ["calendar.family", "calendar.work"],
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: "List view with title",
|
||||
config: {
|
||||
type: "calendar",
|
||||
title: "This week",
|
||||
initial_view: "listWeek",
|
||||
entities: ["calendar.family", "calendar.work"],
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: "Day view",
|
||||
config: {
|
||||
type: "calendar",
|
||||
initial_view: "dayGridDay",
|
||||
entities: ["calendar.family", "calendar.work"],
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: "Add event button",
|
||||
config: {
|
||||
type: "calendar",
|
||||
show_add_event: true,
|
||||
entities: ["calendar.family"],
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: "Failing calendar",
|
||||
config: {
|
||||
type: "calendar",
|
||||
initial_view: "listWeek",
|
||||
entities: ["calendar.family", "calendar.holidays"],
|
||||
},
|
||||
},
|
||||
] satisfies DemoCardConfig<CalendarCardConfig>[];
|
||||
|
||||
@customElement("demo-lovelace-calendar-card")
|
||||
class DemoCalendarCard extends LitElement {
|
||||
@state() private hass?: HomeAssistant;
|
||||
|
||||
public provideHass(el) {
|
||||
el.hass = this.hass;
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("show-dialog", this._dialogManager);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("show-dialog", this._dialogManager);
|
||||
}
|
||||
|
||||
// Opens the event detail and editor dialogs of the calendar
|
||||
private _dialogManager = (e: HASSDomEvent<ShowDialogParams<unknown>>) => {
|
||||
const { dialogTag, dialogImport, dialogParams, addHistory, parentElement } =
|
||||
e.detail;
|
||||
showDialog(
|
||||
this,
|
||||
dialogTag,
|
||||
dialogParams,
|
||||
dialogImport,
|
||||
parentElement,
|
||||
addHistory
|
||||
);
|
||||
};
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<demo-cards
|
||||
.hass=${this.hass}
|
||||
.configs=${CONFIGS}
|
||||
></demo-cards>`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProperties);
|
||||
// Provided on this element, so the dialogs it opens get the mocks too
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
mockCalendars(hass);
|
||||
hass.addEntities(CALENDAR_ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-lovelace-calendar-card": DemoCalendarCard;
|
||||
}
|
||||
}
|
||||
@@ -271,7 +271,7 @@ const CONFIGS = [
|
||||
|
||||
### ha-alert
|
||||
|
||||
You can use our [\`ha-alert\`](https://contributors.home-assistant.io/design/components/alert/) component in markdown content rendered in the Home Assistant Frontend.
|
||||
You can use our [\`ha-alert\`](https://design.home-assistant.io/#components/ha-alert) component in markdown content rendered in the Home Assistant Frontend.
|
||||
|
||||
<ha-alert alert-type="error">This is an error alert — check it out!</ha-alert>
|
||||
<ha-alert alert-type="warning">This is a warning alert — check it out!</ha-alert>
|
||||
|
||||
@@ -102,13 +102,6 @@ const CONFIGS = [
|
||||
entity: "media_player.theater_off_static",
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: "Player Off (assumed state, cannot be switched on)",
|
||||
config: {
|
||||
type: "media-control",
|
||||
entity: "media_player.theater_off_assumed",
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: "Player On (cannot be switched off)",
|
||||
config: {
|
||||
|
||||
@@ -81,15 +81,6 @@ const CONFIGS = [
|
||||
state_color: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: "With custom color",
|
||||
config: {
|
||||
type: "sensor",
|
||||
entity: "sensor.outside_temperature",
|
||||
color: "teal",
|
||||
graph: "line",
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: "With name, icon and unit",
|
||||
config: {
|
||||
|
||||
@@ -109,29 +109,6 @@ const disabledFailedUnloadEntry = createConfigEntry(
|
||||
}
|
||||
);
|
||||
|
||||
const brandEntries: ConfigEntryExtended[] = (
|
||||
[
|
||||
["hue", "Philips Hue", "loaded"],
|
||||
["hue", "Philips Hue", "setup_error"],
|
||||
["mqtt", "MQTT", "setup_retry"],
|
||||
["zha", "Zigbee Home Automation", "not_loaded"],
|
||||
["sonos", "Sonos", "setup_error"],
|
||||
["shelly", "Shelly", "failed_unload"],
|
||||
["spotify", "Spotify", "setup_retry"],
|
||||
["tuya", "Tuya", "setup_error"],
|
||||
["unifi", "UniFi Network", "not_loaded"],
|
||||
["met", "Meteorologisk institutt (Met.no)", "setup_error"],
|
||||
["google_assistant", "Google Assistant", "not_loaded"],
|
||||
["homekit_controller", "HomeKit Device", "setup_retry"],
|
||||
] as [string, string, ConfigEntryExtended["state"]][]
|
||||
).map(([domain, name, entryState]) =>
|
||||
createConfigEntry(`${name} ${entryState}`, {
|
||||
domain,
|
||||
localized_domain_name: name,
|
||||
state: entryState,
|
||||
})
|
||||
);
|
||||
|
||||
const configFlows: DataEntryFlowProgressExtended[] = [
|
||||
{
|
||||
flow_id: "adbb401329d8439ebb78ef29837826a8",
|
||||
@@ -313,6 +290,7 @@ export class DemoIntegrationCard extends LitElement {
|
||||
class=${classMap({
|
||||
highlight: info.highlight !== undefined,
|
||||
})}
|
||||
.hass=${this.hass}
|
||||
domain="esphome"
|
||||
.items=${info.items}
|
||||
.manifest=${createManifest(
|
||||
@@ -335,6 +313,7 @@ export class DemoIntegrationCard extends LitElement {
|
||||
<!-- One that is standalone to see how it increases height if height
|
||||
not defined by other cards. -->
|
||||
<ha-integration-card
|
||||
.hass=${this.hass}
|
||||
domain="esphome"
|
||||
.items=${[
|
||||
loadedEntry,
|
||||
@@ -348,21 +327,6 @@ export class DemoIntegrationCard extends LitElement {
|
||||
.deviceRegistryEntries=${createDeviceRegistryEntries(loadedEntry)}
|
||||
></ha-integration-card>
|
||||
</div>
|
||||
<div class="container">
|
||||
${brandEntries.map(
|
||||
(entry) => html`
|
||||
<ha-integration-card
|
||||
.domain=${entry.domain}
|
||||
.items=${[entry]}
|
||||
.manifest=${createManifest(
|
||||
this.isCustomIntegration,
|
||||
this.isCloud
|
||||
)}
|
||||
.entityRegistryEntries=${createEntityRegistryEntries(entry)}
|
||||
></ha-integration-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
title: Calendar
|
||||
---
|
||||
@@ -1,74 +0,0 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { ShowDialogParams } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/dialogs/more-info/more-info-content";
|
||||
import type { MockHomeAssistant } from "../../../../src/fake_data/provide_hass";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import "../../components/demo-more-infos";
|
||||
import { CALENDAR_ENTITIES, mockCalendars } from "../../data/calendar";
|
||||
|
||||
const ENTITIES = ["calendar.family", "calendar.empty", "calendar.holidays"];
|
||||
|
||||
@customElement("demo-more-info-calendar")
|
||||
class DemoMoreInfoCalendar extends LitElement {
|
||||
@property({ attribute: false }) public hass!: MockHomeAssistant;
|
||||
|
||||
public provideHass(el) {
|
||||
el.hass = this.hass;
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("show-dialog", this._dialogManager);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("show-dialog", this._dialogManager);
|
||||
}
|
||||
|
||||
// Opens the event detail and editor dialogs of the calendar
|
||||
private _dialogManager = (e: HASSDomEvent<ShowDialogParams<unknown>>) => {
|
||||
const { dialogTag, dialogImport, dialogParams, addHistory, parentElement } =
|
||||
e.detail;
|
||||
showDialog(
|
||||
this,
|
||||
dialogTag,
|
||||
dialogParams,
|
||||
dialogImport,
|
||||
parentElement,
|
||||
addHistory
|
||||
);
|
||||
};
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<demo-more-infos
|
||||
.hass=${this.hass}
|
||||
.entities=${ENTITIES}
|
||||
></demo-more-infos>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProperties);
|
||||
// Provided on this element, so the dialogs it opens get the mocks too
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
mockCalendars(hass);
|
||||
hass.addEntities(CALENDAR_ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-more-info-calendar": DemoMoreInfoCalendar;
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ It is similar to the core frontend dev.
|
||||
- `"mounts": ["source=/path/to/hass/frontend,target=/workspaces/frontend,type=bind,consistency=cached"]`
|
||||
- use the dev container
|
||||
- start the dev server with following optional env vars:
|
||||
- `SUPERVISOR_HOST` to have real supervisor data, you can [setup a supervisor remote API access](https://contributors.home-assistant.io/os/supervisor/core-connection/#get-access-to-the-supervisor-api) and set the host of your supervisor. e.g.: `SUPERVISOR_HOST=192.168.0.20:8888`
|
||||
- `SUPERVISOR_HOST` to have real supervisor data, you can [setup a supervisor remote API access](https://developers.home-assistant.io/docs/supervisor/development/#supervisor-api-access) and set the host of your supervisor. e.g.: `SUPERVISOR_HOST=192.168.0.20:8888`
|
||||
- `SUPERVISOR_TOKEN` the supervisor api token you get from the Remote API proxy Addon Logs
|
||||
- `FRONTEND_PATH` the path inside your container should be `/workspaces/frontend`
|
||||
- example: `SUPERVISOR_TOKEN=abc123 SUPERVISOR_HOST=192.168.0.20:8888 FRONTEND_PATH=/workspaces/frontend go run main.go http.go mdns.go`
|
||||
|
||||
@@ -21,13 +21,10 @@ import {
|
||||
getObserverLogs,
|
||||
downloadUrl as observerLogsDownloadUrl,
|
||||
} from "../data/observer";
|
||||
import {
|
||||
getSupervisorLogs,
|
||||
getSupervisorLogsFollow,
|
||||
isSupervisorLogError,
|
||||
} from "../data/supervisor";
|
||||
import { getSupervisorLogs, getSupervisorLogsFollow } from "../data/supervisor";
|
||||
import { ASSUME_CORE_START_SECONDS } from "../ha-landing-page";
|
||||
|
||||
const ERROR_CHECK = /^[\d\s-:]+(ERROR|CRITICAL)(.*)/gm;
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"landing-page-error": undefined;
|
||||
@@ -64,8 +61,6 @@ class LandingPageLogs extends LitElement {
|
||||
|
||||
@state() private _logLinesCount = 0;
|
||||
|
||||
private _errorFound = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="actions">
|
||||
@@ -165,28 +160,26 @@ class LandingPageLogs extends LitElement {
|
||||
this._logLinesCount = 0;
|
||||
}
|
||||
|
||||
const showError = ERROR_CHECK.test(logs);
|
||||
|
||||
const scrolledToBottom = this._scrolledToBottomController.value;
|
||||
const lines = `${tempLogLine}${logs}`
|
||||
.split("\n")
|
||||
.filter((line) => line.trim() !== "");
|
||||
|
||||
// handle edge case where the last line is not complete
|
||||
if (clear || logs.endsWith("\n")) {
|
||||
if (logs.endsWith("\n")) {
|
||||
tempLogLine = "";
|
||||
} else {
|
||||
tempLogLine = lines.splice(-1, 1)[0];
|
||||
}
|
||||
|
||||
const showError = lines.some(isSupervisorLogError);
|
||||
|
||||
if (lines.length) {
|
||||
this._ansiToHtmlElement?.parseLinesToColoredPre(lines);
|
||||
this._logLinesCount += lines.length;
|
||||
}
|
||||
|
||||
// only open the logs once, so they can be closed again
|
||||
if (showError && !this._errorFound) {
|
||||
this._errorFound = true;
|
||||
if (showError) {
|
||||
fireEvent(this, "landing-page-error");
|
||||
this._show = true;
|
||||
}
|
||||
|
||||
@@ -4,17 +4,8 @@ import type {
|
||||
DockerNetwork,
|
||||
NetworkInterface,
|
||||
} from "../../../src/data/hassio/network";
|
||||
import type { HassioResolution } from "../../../src/data/hassio/resolution";
|
||||
import { handleFetchPromise } from "../../../src/util/hass-call-api";
|
||||
|
||||
// Supervisor log lines start with a color code and a timestamp, for example
|
||||
// "\x1b[31m2026-10-21 22:27:32.135+02:00 ERROR (MainThread) ..."
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const LOG_ERROR_LINE = /^(?:\x1b\[[0-9;]*m)*[\d\s.:+-]+(?:ERROR|CRITICAL)\b/;
|
||||
|
||||
export const isSupervisorLogError = (line: string): boolean =>
|
||||
LOG_ERROR_LINE.test(line);
|
||||
|
||||
export interface NetworkInfo {
|
||||
interfaces: NetworkInterface[];
|
||||
docker: DockerNetwork;
|
||||
@@ -88,13 +79,6 @@ export async function getSupervisorNetworkInfo(): Promise<NetworkInfo> {
|
||||
return responseData?.data;
|
||||
}
|
||||
|
||||
export async function getSupervisorResolutionInfo(): Promise<HassioResolution> {
|
||||
const responseData = await handleFetchPromise<
|
||||
HassioResponse<HassioResolution>
|
||||
>(fetch("/supervisor-api/resolution/info"));
|
||||
return responseData?.data;
|
||||
}
|
||||
|
||||
export async function getSupervisorJobsInfo(): Promise<
|
||||
HassioResponse<SupervisorJobInfo>
|
||||
> {
|
||||
|
||||
@@ -18,7 +18,6 @@ import "./components/landing-page-network";
|
||||
import {
|
||||
getSupervisorJobsInfo,
|
||||
getSupervisorNetworkInfo,
|
||||
getSupervisorResolutionInfo,
|
||||
pingSupervisor,
|
||||
type NetworkInfo,
|
||||
} from "./data/supervisor";
|
||||
@@ -50,9 +49,6 @@ class HaLandingPage extends provideLiteI18nMixin(LandingPageBaseElement) {
|
||||
|
||||
render() {
|
||||
const networkIssue = this._networkInfo && !this._networkInfo.host_internet;
|
||||
// Supervisor retries some failures, so keep showing a running install
|
||||
const showProgress =
|
||||
!networkIssue && (!this._supervisorError || this._progress >= 0);
|
||||
|
||||
if (!this.localize) {
|
||||
return html`
|
||||
@@ -67,7 +63,7 @@ class HaLandingPage extends provideLiteI18nMixin(LandingPageBaseElement) {
|
||||
<div class="card-content">
|
||||
<h1>${this.localize("header")}</h1>
|
||||
${
|
||||
showProgress
|
||||
!networkIssue && !this._supervisorError
|
||||
? html`
|
||||
<p>${this.localize("subheader")}</p>
|
||||
<ha-progress-bar
|
||||
@@ -183,8 +179,6 @@ class HaLandingPage extends provideLiteI18nMixin(LandingPageBaseElement) {
|
||||
this._networkInfo = await getSupervisorNetworkInfo();
|
||||
this._networkInfoError = false;
|
||||
this._coreStatusChecked = false;
|
||||
|
||||
await this._fetchSupervisorResolutionInfo();
|
||||
} catch (err: any) {
|
||||
if (await this._checkCoreAvailability()) {
|
||||
// core is available, page reload in progress -> don't show an error
|
||||
@@ -204,19 +198,6 @@ class HaLandingPage extends provideLiteI18nMixin(LandingPageBaseElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchSupervisorResolutionInfo() {
|
||||
try {
|
||||
const resolutionInfo = await getSupervisorResolutionInfo();
|
||||
if (
|
||||
resolutionInfo.issues.some((issue) => issue.type === "update_failed")
|
||||
) {
|
||||
this._supervisorError = true;
|
||||
}
|
||||
} catch (_err: any) {
|
||||
// Ignore, the logs are checked for errors as well
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchSupervisorJobsInfo() {
|
||||
try {
|
||||
const jobsInfo = await getSupervisorJobsInfo();
|
||||
|
||||
@@ -6,7 +6,7 @@ export default {
|
||||
],
|
||||
"*.{json,css,md,markdown,html,ya?ml}": "prettier --cache --write",
|
||||
"translations/*/*.json": (files) =>
|
||||
'printf "%s\n" "Translation files should not be added or modified here. Instead, make the necessary modifications in src/translations/en.json. Other languages are managed externally. Please see https://contributors.home-assistant.io/translate/ for details." ' +
|
||||
'printf "%s\n" "Translation files should not be added or modified here. Instead, make the necessary modifications in src/translations/en.json. Other languages are managed externally. Please see https://developers.home-assistant.io/docs/translations/ for details." ' +
|
||||
files.join(" ") +
|
||||
" >&2 && exit 1",
|
||||
"pnpm-lock.yaml": () => "pnpm dedupe",
|
||||
|
||||
@@ -40,31 +40,36 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "8.0.8",
|
||||
"@babel/runtime": "8.0.5",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.4",
|
||||
"@codemirror/autocomplete": "6.20.3",
|
||||
"@codemirror/commands": "6.11.1",
|
||||
"@codemirror/lang-jinja": "6.0.1",
|
||||
"@codemirror/lang-python": "6.2.1",
|
||||
"@codemirror/lang-yaml": "6.1.3",
|
||||
"@codemirror/language": "6.13.1",
|
||||
"@codemirror/language": "6.12.4",
|
||||
"@codemirror/lint": "6.9.7",
|
||||
"@codemirror/search": "6.7.2",
|
||||
"@codemirror/state": "6.7.6",
|
||||
"@codemirror/view": "6.43.15",
|
||||
"@codemirror/view": "6.43.13",
|
||||
"@date-fns/tz": "1.5.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.8.2",
|
||||
"@formatjs/intl-datetimeformat": "7.8.1",
|
||||
"@formatjs/intl-displaynames": "7.3.15",
|
||||
"@formatjs/intl-durationformat": "0.11.1",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.12",
|
||||
"@formatjs/intl-listformat": "8.3.15",
|
||||
"@formatjs/intl-locale": "5.3.12",
|
||||
"@formatjs/intl-numberformat": "9.4.3",
|
||||
"@formatjs/intl-pluralrules": "6.3.16",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.16",
|
||||
"@home-assistant/webawesome": "3.14.0-ha.2",
|
||||
"@lezer/common": "1.5.4",
|
||||
"@formatjs/intl-pluralrules": "6.3.15",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.15",
|
||||
"@fullcalendar/core": "6.1.21",
|
||||
"@fullcalendar/daygrid": "6.1.21",
|
||||
"@fullcalendar/interaction": "6.1.21",
|
||||
"@fullcalendar/list": "6.1.21",
|
||||
"@fullcalendar/luxon3": "6.1.21",
|
||||
"@fullcalendar/timegrid": "6.1.21",
|
||||
"@home-assistant/webawesome": "3.14.0-ha.0",
|
||||
"@lezer/common": "1.5.3",
|
||||
"@lezer/highlight": "1.2.5",
|
||||
"@lit-labs/motion": "1.1.0",
|
||||
"@lit-labs/observers": "2.1.0",
|
||||
@@ -80,37 +85,36 @@
|
||||
"@swc/helpers": "0.5.23",
|
||||
"@tsparticles/engine": "4.4.0",
|
||||
"@tsparticles/preset-links": "4.4.0",
|
||||
"@versatiles/style": "7.0.1",
|
||||
"@versatiles/style": "6.1.1",
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@vvo/tzdb": "6.201.1",
|
||||
"@vvo/tzdb": "6.198.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"barcode-detector": "3.2.3",
|
||||
"barcode-detector": "3.2.2",
|
||||
"cally": "0.9.2",
|
||||
"color-name": "2.1.1",
|
||||
"comlink": "4.4.2",
|
||||
"core-js": "3.51.0",
|
||||
"cropperjs": "1.7.0",
|
||||
"core-js": "3.50.0",
|
||||
"cropperjs": "1.6.3",
|
||||
"culori": "4.0.2",
|
||||
"date-fns": "4.4.0",
|
||||
"decimal.js": "10.6.0",
|
||||
"deep-clone-simple": "1.1.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"echarts": "6.1.0",
|
||||
"echarts-extension-chart2music": "0.1.1",
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fullcalendar": "7.1.3",
|
||||
"fuse.js": "7.5.0",
|
||||
"hls.js": "1.7.3",
|
||||
"home-assistant-js-websocket": "9.7.1",
|
||||
"idb-keyval": "6.3.0",
|
||||
"intl-messageformat": "12.1.4",
|
||||
"js-yaml": "5.5.0",
|
||||
"intl-messageformat": "12.1.3",
|
||||
"js-yaml": "5.4.3",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
"lit": "3.3.3",
|
||||
"lit-html": "3.3.3",
|
||||
"maplibre-gl": "6.15.0",
|
||||
"marked": "18.1.1",
|
||||
"luxon": "3.7.2",
|
||||
"maplibre-gl": "6.12.0",
|
||||
"marked": "18.1.0",
|
||||
"memoize-one": "6.0.0",
|
||||
"nanoid": "6.0.2",
|
||||
"node-vibrant": "4.0.4",
|
||||
@@ -123,7 +127,6 @@
|
||||
"sortablejs": "1.15.7",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "2.0.2",
|
||||
"temporal-polyfill": "1.0.6",
|
||||
"tinykeys": "4.0.1",
|
||||
"weekstart": "2.0.0",
|
||||
"workbox-cacheable-response": "7.4.1",
|
||||
@@ -132,31 +135,30 @@
|
||||
"workbox-precaching": "7.4.1",
|
||||
"workbox-routing": "7.4.1",
|
||||
"workbox-strategies": "7.4.1",
|
||||
"xss": "1.0.17",
|
||||
"zrender": "6.1.0"
|
||||
"xss": "1.0.15",
|
||||
"zrender": "6.1.0",
|
||||
"zxing-wasm": "3.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ampproject/remapping": "2.3.0",
|
||||
"@babel/core": "8.0.9",
|
||||
"@babel/helper-compilation-targets": "8.0.7",
|
||||
"@babel/core": "8.0.6",
|
||||
"@babel/helper-compilation-targets": "8.0.6",
|
||||
"@babel/helper-define-polyfill-provider": "1.0.0",
|
||||
"@babel/plugin-transform-class-properties": "8.0.1",
|
||||
"@babel/plugin-transform-private-methods": "8.0.1",
|
||||
"@babel/plugin-transform-runtime": "8.0.6",
|
||||
"@babel/preset-env": "8.0.7",
|
||||
"@babel/preset-env": "8.0.6",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.3",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@gfx/zopfli": "1.0.15",
|
||||
"@html-eslint/eslint-plugin": "0.67.0",
|
||||
"@lokalise/node-api": "17.0.1",
|
||||
"@html-eslint/eslint-plugin": "0.66.1",
|
||||
"@lokalise/node-api": "16.6.0",
|
||||
"@octokit/auth-oauth-device": "8.0.5",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@playwright/test": "1.64.1",
|
||||
"@rsdoctor/rspack-plugin": "1.6.5",
|
||||
"@rspack/core": "2.2.9",
|
||||
"@playwright/test": "1.63.0",
|
||||
"@rsdoctor/rspack-plugin": "1.6.4",
|
||||
"@rspack/core": "2.2.8",
|
||||
"@rspack/dev-server": "2.2.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "7.0.1",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
"@types/color-name": "2.0.0",
|
||||
"@types/culori": "4.0.1",
|
||||
@@ -165,24 +167,25 @@
|
||||
"@types/leaflet": "1.9.22",
|
||||
"@types/leaflet.markercluster": "1.5.6",
|
||||
"@types/lodash.merge": "4.6.9",
|
||||
"@types/node": "24.19.2",
|
||||
"@types/luxon": "3.7.6",
|
||||
"@types/node": "24.19.1",
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "7.0.87",
|
||||
"@typescript/native": "npm:[email protected]",
|
||||
"@vitest/coverage-v8": "5.0.5",
|
||||
"@vitest/coverage-v8": "5.0.3",
|
||||
"babel-loader": "10.1.1",
|
||||
"babel-plugin-polyfill-corejs3": "1.0.0",
|
||||
"browserslist": "4.29.4",
|
||||
"browserslist": "4.29.3",
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
"core-js-compat": "3.51.0",
|
||||
"del": "9.0.1",
|
||||
"eslint": "10.13.0",
|
||||
"core-js-compat": "3.50.0",
|
||||
"del": "8.0.1",
|
||||
"eslint": "10.12.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.11",
|
||||
"eslint-plugin-import-x": "4.17.1",
|
||||
"eslint-plugin-lit": "2.3.1",
|
||||
"eslint-plugin-lit-a11y": "5.1.3",
|
||||
"eslint-plugin-lit-a11y": "5.1.1",
|
||||
"eslint-plugin-unused-imports": "4.4.1",
|
||||
"eslint-plugin-wc": "3.1.0",
|
||||
"fancy-log": "2.0.0",
|
||||
@@ -195,7 +198,7 @@
|
||||
"gulp-rename": "2.1.0",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "30.1.3",
|
||||
"jsdom": "30.1.2",
|
||||
"jszip": "3.10.2",
|
||||
"license-checker-rseidelsohn": "5.0.1",
|
||||
"lightningcss": "1.33.0",
|
||||
@@ -206,7 +209,7 @@
|
||||
"map-stream": "0.0.7",
|
||||
"minify-literals": "2.2.0",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.9.10",
|
||||
"prettier": "3.9.9",
|
||||
"rspack-manifest-plugin": "5.2.2",
|
||||
"serve": "14.2.6",
|
||||
"sinon": "22.1.0",
|
||||
@@ -214,16 +217,16 @@
|
||||
"terser-webpack-plugin": "5.6.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.72.0",
|
||||
"vite": "8.3.5",
|
||||
"typescript-eslint": "8.71.1",
|
||||
"vite": "8.3.3",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "5.0.5",
|
||||
"vitest": "5.0.3",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "7.4.1"
|
||||
},
|
||||
"packageManager": "[email protected]5.0",
|
||||
"packageManager": "[email protected]0.0",
|
||||
"volta": {
|
||||
"node": "24.22.1"
|
||||
"node": "24.21.0"
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ overrides:
|
||||
lit-html: 3.3.3
|
||||
clean-css: 5.3.3
|
||||
"@lit/reactive-element": 2.1.2
|
||||
"@fullcalendar/daygrid": 6.1.21
|
||||
tslib: 2.8.1
|
||||
|
||||
patchedDependencies:
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<svg width="94" height="28" viewBox="0 0 94 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="28" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="27" rx="7.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<rect x="8" y="8" width="78" height="12" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="12" y="13.25" width="6" height="1.5" rx="0.75" fill="black" fill-opacity="0.48"/>
|
||||
<rect x="37" y="12.5" width="20" height="3" rx="1.5" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="76" y="13.25" width="6" height="1.5" rx="0.75" fill="black" fill-opacity="0.48"/>
|
||||
<rect x="78.25" y="11" width="1.5" height="6" rx="0.75" fill="black" fill-opacity="0.48"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 691 B |
@@ -1,9 +0,0 @@
|
||||
<svg width="94" height="28" viewBox="0 0 94 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="28" rx="8" fill="#1C1C1C"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="27" rx="7.5" stroke="white" stroke-opacity="0.24"/>
|
||||
<rect x="8" y="8" width="78" height="12" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="12" y="13.25" width="6" height="1.5" rx="0.75" fill="white" fill-opacity="0.72"/>
|
||||
<rect x="37" y="12.5" width="20" height="3" rx="1.5" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="76" y="13.25" width="6" height="1.5" rx="0.75" fill="white" fill-opacity="0.72"/>
|
||||
<rect x="78.25" y="11" width="1.5" height="6" rx="0.75" fill="white" fill-opacity="0.72"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 693 B |
@@ -1,7 +0,0 @@
|
||||
<svg width="94" height="28" viewBox="0 0 94 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="28" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="27" rx="7.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<rect x="8" y="8" width="78" height="12" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="8" y="8" width="46" height="12" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="49" y="10.5" width="2" height="7" rx="1" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 478 B |
@@ -1,7 +0,0 @@
|
||||
<svg width="94" height="28" viewBox="0 0 94 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="28" rx="8" fill="#1C1C1C"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="27" rx="7.5" stroke="white" stroke-opacity="0.24"/>
|
||||
<rect x="8" y="8" width="78" height="12" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="8" y="8" width="46" height="12" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="49" y="10.5" width="2" height="7" rx="1" fill="#1C1C1C"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 482 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="36" height="16" viewBox="0 0 36 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 252 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="36" height="16" viewBox="0 0 36 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 252 B |
@@ -1,5 +0,0 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 330 B |
@@ -1,5 +0,0 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 330 B |
@@ -1,6 +0,0 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 408 B |
@@ -1,6 +0,0 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 408 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="36" height="16" viewBox="0 0 36 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 174 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="36" height="16" viewBox="0 0 36 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 174 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 252 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 252 B |
@@ -1,5 +0,0 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 330 B |
@@ -1,5 +0,0 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 330 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="56" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 174 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="56" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 174 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="56" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 252 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="56" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 252 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="76" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 174 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="76" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 174 B |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 223 KiB |
@@ -98,11 +98,6 @@
|
||||
"groupName": "date-fns",
|
||||
"matchPackageNames": ["date-fns", "date-fns-tz"]
|
||||
},
|
||||
{
|
||||
"description": "Group ECharts with zrender, which we import directly and must match the version echarts depends on",
|
||||
"groupName": "echarts",
|
||||
"matchPackageNames": ["echarts", "zrender"]
|
||||
},
|
||||
{
|
||||
"description": "Group formatjs monorepo package",
|
||||
"groupName": "formatjs",
|
||||
|
||||