Compare commits

..

16 Commits

Author SHA1 Message Date
Paul Bottein
134681b4c9 Merge branch 'dev' into toggle_group_dialog 2025-07-10 18:50:14 +02:00
Paul Bottein
082f1ca55e Center content on mobile 2025-07-10 18:49:05 +02:00
Paul Bottein
341e63e878 Fix device class icon off state 2025-07-10 17:36:03 +02:00
Paul Bottein
5ed2d2fd2f Fix last updated 2025-07-10 17:33:15 +02:00
Paul Bottein
c6f92d1375 Add translations 2025-07-10 17:26:36 +02:00
Paul Bottein
e8201f7848 Use variable 2025-07-10 15:59:53 +02:00
Paul Bottein
6d7df18e82 Fix available entities and header 2025-07-10 15:58:09 +02:00
Paul Bottein
1471cfea66 Don't use new colors for now 2025-07-10 15:33:29 +02:00
Paul Bottein
9e4835107d Merge dialog with more info 2025-07-10 14:43:46 +02:00
Paul Bottein
3269fd3c5b Feedbacks 2025-07-09 18:14:29 +02:00
Paul Bottein
17e63343c7 Handle multiple entities 2025-07-09 16:52:58 +02:00
Paul Bottein
dc7ba0dac6 Fix dialog at the top 2025-07-09 10:02:12 +02:00
Paul Bottein
2ab4608884 Delete dashboard dialog 2025-07-09 09:49:44 +02:00
Paul Bottein
de7f5c1bb7 Add toggle group dialog 2025-07-08 19:18:13 +02:00
Paul Bottein
7144b7802e Invert order 2025-07-08 15:06:59 +02:00
Paul Bottein
ca315b88ce Add sections dialog 2025-07-08 12:35:51 +02:00
2079 changed files with 78087 additions and 169514 deletions

View File

@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/devcontainers/python:3.14
FROM mcr.microsoft.com/devcontainers/python:1-3.13
ENV \
DEBIAN_FRONTEND=noninteractive \

View File

@@ -67,7 +67,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
<!--
If your issue is about how an entity is shown in the UI, please add the state
and attributes for all situations with a screenshot of the UI.
You can find this information at `/config/developer-tools/state`
You can find this information at `/developer-tools/state`
-->
```yaml

View File

@@ -3,9 +3,6 @@ contact_links:
- name: Request a feature for the UI / Dashboards
url: https://github.com/orgs/home-assistant/discussions
about: Request a new feature for the Home Assistant frontend.
- name: Discuss UI or UX design
url: https://github.com/OpenHomeFoundation/ux-design/discussions
about: Share design feedback and discuss visual or UX changes with the design team.
- name: Report a bug that is NOT related to the UI / Dashboards
url: https://github.com/home-assistant/core/issues
about: This is the issue tracker for our frontend. Please report other issues in the backend ("core") repository.

View File

@@ -21,14 +21,6 @@
-->
## Screenshots
<!--
If your PR includes visual changes, please add screenshots or a short video
showing the before and after. This helps reviewers understand the impact of
your changes.
Note: Remove this section if this PR has no visual changes.
-->
## Type of change
<!--
What type of change does your PR introduce to the Home Assistant frontend?
@@ -43,6 +35,16 @@
- [ ] Breaking change (fix/feature causing existing functionality to break)
- [ ] Code quality improvements to existing code or addition of tests
## Example configuration
<!--
Supplying a configuration snippet, makes it easier for a maintainer to test
your PR.
-->
```yaml
```
## Additional information
<!--
Details are important, and help maintainers processing your PR.
@@ -52,8 +54,6 @@
- This PR fixes or closes issue: fixes #
- This PR is related to issue or discussion:
- Link to documentation pull request:
- Link to developer documentation pull request:
- Link to backend pull request:
## Checklist
<!--
@@ -61,48 +61,18 @@
creating the PR. If you're unsure about any of them, don't hesitate to ask.
We're here to help! This is simply a reminder of what we are going to look
for before merging your code.
AI tools are welcome, but contributors are responsible for *fully*
understanding the code before submitting a PR.
-->
- [ ] I understand the code I am submitting and can explain how it works.
- [ ] The code change is tested and works locally.
- [ ] There is no commented out code in this PR.
- [ ] I have followed the [perfect PR recommendations][perfect-pr]
- [ ] Any generated code has been carefully reviewed for correctness and compliance with project standards.
- [ ] Tests have been added to verify that the new code works.
If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository]
<!--
This project is very active and we have a high turnover of pull requests.
Unfortunately, the number of incoming pull requests is higher than what our
reviewers can review and merge so there is a long backlog of pull requests
waiting for review. You can help here!
By reviewing another pull request, you will help raise the code quality of
that pull request and the final review will be faster. This way the general
pace of pull request reviews will go up and your wait time will go down.
When picking a pull request to review, try to choose one that hasn't yet
been reviewed.
Thanks for helping out!
-->
To help with the load of incoming pull requests:
- [ ] I have reviewed two other [open pull requests][prs] in this repository.
[prs]: https://github.com/home-assistant/frontend/pulls?q=is%3Aopen+is%3Apr+-author%3A%40me+-draft%3Atrue+sort%3Acreated-desc+review%3Anone+-status%3Afailure
<!--
Thank you for contributing <3
Below, some useful links you could explore:
-->
[docs-repository]: https://github.com/home-assistant/home-assistant.io
[perfect-pr]: https://developers.home-assistant.io/docs/review-process/#creating-the-perfect-pr

View File

@@ -2,8 +2,6 @@
You are an assistant helping with development of the Home Assistant frontend. The frontend is built using Lit-based Web Components and TypeScript, providing a responsive and performant interface for home automation control.
**Note**: This file contains high-level guidelines and references to implementation patterns. For detailed component documentation, API references, and usage examples, refer to the `gallery/` directory.
## Table of Contents
- [Quick Reference](#quick-reference)
@@ -22,13 +20,11 @@ You are an assistant helping with development of the Home Assistant frontend. Th
```bash
yarn lint # ESLint + Prettier + TypeScript + Lit
yarn format # Auto-fix ESLint + Prettier
yarn lint:types # TypeScript compiler (run WITHOUT file arguments)
yarn lint:types # TypeScript compiler
yarn test # Vitest
script/develop # Development server
```
> **WARNING:** Never run `tsc` or `yarn lint:types` with file arguments (e.g., `yarn lint:types src/file.ts`). When `tsc` receives file arguments, it ignores `tsconfig.json` and emits `.js` files into `src/`, polluting the codebase. Always run `yarn lint:types` without arguments. For individual file type checking, rely on IDE diagnostics. If `.js` files are accidentally generated, clean up with `git clean -fd src/`.
### Component Prefixes
- `ha-` - Home Assistant components
@@ -155,10 +151,6 @@ try {
### Styling Guidelines
- **Use CSS custom properties**: Leverage the theme system
- **Use spacing tokens**: Prefer `--ha-space-*` tokens over hardcoded values for consistent spacing
- Spacing scale: `--ha-space-1` (4px) through `--ha-space-20` (80px) in 4px increments
- Defined in `src/resources/theme/core.globals.ts`
- Common values: `--ha-space-2` (8px), `--ha-space-4` (16px), `--ha-space-8` (32px)
- **Mobile-first responsive**: Design for mobile, enhance for desktop
- **Follow Material Design**: Use Material Web Components where appropriate
- **Support RTL**: Ensure all layouts work in RTL languages
@@ -167,61 +159,21 @@ try {
static get styles() {
return css`
:host {
padding: var(--ha-space-4);
--spacing: 16px;
padding: var(--spacing);
color: var(--primary-text-color);
background-color: var(--card-background-color);
}
.content {
gap: var(--ha-space-2);
}
@media (max-width: 600px) {
:host {
padding: var(--ha-space-2);
--spacing: 8px;
}
}
`;
}
```
### View Transitions
The View Transitions API creates smooth animations between DOM state changes. When implementing view transitions:
**Core Resources:**
- **Utility wrapper**: `src/common/util/view-transition.ts` - `withViewTransition()` function with graceful fallback
- **Real-world example**: `src/util/launch-screen.ts` - Launch screen fade pattern with browser support detection
- **Animation keyframes**: `src/resources/theme/animations.globals.ts` - Global `fade-in`, `fade-out`, `scale` animations
- **Animation duration**: `src/resources/theme/core.globals.ts` - `--ha-animation-duration-fast` (150ms), `--ha-animation-duration-normal` (250ms), `--ha-animation-duration-slow` (350ms) (all respect `prefers-reduced-motion`)
**Implementation Guidelines:**
1. Always use `withViewTransition()` wrapper for automatic fallback
2. Keep transitions simple (subtle crossfades and fades work best)
3. Use `--ha-animation-duration-*` CSS variables for consistent timing (`fast`, `normal`, `slow`)
4. Assign unique `view-transition-name` to elements (must be unique at any given time)
5. For Lit components: Override `performUpdate()` or use `::part()` for internal elements
**Default Root Transition:**
By default, `:root` receives `view-transition-name: root`, creating a full-page crossfade. Target with [`::view-transition-group(root)`](https://developer.mozilla.org/en-US/docs/Web/CSS/::view-transition-group) to customize the default page transition.
**Important Constraints:**
- Each `view-transition-name` must be unique at any given time
- Only one view transition can run at a time
- **Shadow DOM incompatibility**: View transitions operate at document level and do not work within Shadow DOM due to style isolation ([spec discussion](https://github.com/w3c/csswg-drafts/issues/10303)). For web components, set `view-transition-name` on the `:host` element or use document-level transitions
**Specification & Documentation:**
For browser support, API details, and current specifications, refer to these authoritative sources (note: check publication dates as specs evolve):
- [MDN: View Transition API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API) - Comprehensive API reference
- [Chrome for Developers: View Transitions](https://developer.chrome.com/docs/web-platform/view-transitions) - Implementation guide and examples
- [W3C Draft Specification](https://drafts.csswg.org/css-view-transitions/) - Official specification (evolving)
### Performance Best Practices
- **Code split**: Split code at the panel/dialog level
@@ -239,7 +191,12 @@ For browser support, API details, and current specifications, refer to these aut
## Component Library
### Dialog Component
### Dialog Components
**Available Dialog Types:**
- `ha-md-dialog` - Preferred for new code (Material Design 3)
- `ha-dialog` - Legacy component still widely used
**Opening Dialogs (Fire Event Pattern - Recommended):**
@@ -253,41 +210,20 @@ fireEvent(this, "show-dialog", {
**Dialog Implementation Requirements:**
- Use `ha-dialog` component
- Implement `HassDialog<T>` interface
- Use `@state() private _open = false` to control dialog visibility
- Set `_open = true` in `showDialog()`, `_open = false` in `closeDialog()`
- Use `createCloseHeading()` for standard headers
- Import `haStyleDialog` for consistent styling
- Return `nothing` when no params (loading state)
- Fire `dialog-closed` event in `_dialogClosed()` handler
- Use `header-title` attribute for simple titles
- Use `header-subtitle` attribute for simple subtitles
- Use slots for custom content where the standard attributes are not enough
- Use `ha-dialog-footer` with `primaryAction`/`secondaryAction` slots for footer content
- Add `autofocus` to first focusable element (e.g., `<ha-form autofocus>`). The component may need to forward this attribute internally.
- Fire `dialog-closed` event when closing
- Add `dialogInitialFocus` for accessibility
**Dialog Sizing:**
- Use `width` attribute with predefined sizes: `"small"` (320px), `"medium"` (560px - default), `"large"` (720px), or `"full"`
- Custom sizing is NOT recommended - use the standard width presets
**Button Appearance Guidelines:**
- **Primary action buttons**: Default appearance (no appearance attribute) or omit for standard styling
- **Secondary action buttons**: Use `appearance="plain"` for cancel/dismiss actions
- **Destructive actions**: Use `appearance="filled"` for delete/remove operations (combined with appropriate semantic styling)
- **Button sizes**: Use `size="small"` (32px height) or default/medium (40px height)
- Always place primary action in `slot="primaryAction"` and secondary in `slot="secondaryAction"` within `ha-dialog-footer`
**Gallery Documentation:**
- `gallery/src/pages/components/ha-dialog.markdown`
- `gallery/src/pages/components/ha-dialogs.markdown`
````
### Form Component (ha-form)
- Schema-driven using `HaFormSchema[]`
- Supports entity, device, area, target, number, boolean, time, action, text, object, select, icon, media, location selectors
- Built-in validation with error display
- Use `dialogInitialFocus` in dialogs
- Use `computeLabel`, `computeError`, `computeHelper` for translations
```typescript
@@ -299,11 +235,7 @@ fireEvent(this, "show-dialog", {
.computeLabel=${(schema) => this.hass.localize(`ui.panel.${schema.name}`)}
@value-changed=${this._valueChanged}
></ha-form>
```
**Gallery Documentation:**
- `gallery/src/pages/components/ha-form.markdown`
````
### Alert Component (ha-alert)
@@ -317,35 +249,6 @@ fireEvent(this, "show-dialog", {
<ha-alert alert-type="success" dismissable>Success message</ha-alert>
```
**Gallery Documentation:**
- `gallery/src/pages/components/ha-alert.markdown`
### Keyboard Shortcuts (ShortcutManager)
The `ShortcutManager` class provides a unified way to register keyboard shortcuts with automatic input field protection.
**Key Features:**
- Automatically blocks shortcuts when input fields are focused
- Prevents shortcuts during text selection (configurable via `allowWhenTextSelected`)
- Supports both character-based and KeyCode-based shortcuts (for non-latin keyboards)
**Implementation:**
- **Class definition**: `src/common/keyboard/shortcuts.ts`
- **Real-world example**: `src/state/quick-bar-mixin.ts` - Global shortcuts (e, c, d, m, a, Shift+?) with non-latin keyboard fallbacks
### Tooltip Component (ha-tooltip)
The `ha-tooltip` component wraps Web Awesome tooltip with Home Assistant theming. Use for providing contextual help text on hover.
**Implementation:**
- **Component definition**: `src/components/ha-tooltip.ts`
- **Usage example**: `src/components/ha-label.ts`
- **Gallery documentation**: `gallery/src/pages/components/ha-tooltip.markdown`
## Common Patterns
### Creating a Panel
@@ -372,6 +275,65 @@ export class HaPanelMyFeature extends SubscribeMixin(LitElement) {
}
```
### Creating a Dialog
```typescript
@customElement("dialog-my-feature")
export class DialogMyFeature
extends LitElement
implements HassDialog<MyDialogParams>
{
@property({ attribute: false })
hass!: HomeAssistant;
@state()
private _params?: MyDialogParams;
public async showDialog(params: MyDialogParams): Promise<void> {
this._params = params;
}
public closeDialog(): void {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(this.hass, this._params.title)}
>
<!-- Dialog content -->
<ha-button @click=${this.closeDialog} slot="secondaryAction">
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button @click=${this._submit} slot="primaryAction">
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog>
`;
}
static styles = [haStyleDialog, css``];
}
```
### Dialog Design Guidelines
- Max width: 560px (Alert/confirmation: 320px fixed width)
- Close X-icon on top left (all screen sizes)
- Submit button grouped with cancel at bottom right
- Keep button labels short: "Save", "Delete", "Enable"
- Destructive actions use red warning button
- Always use a title (best practice)
- Strive for minimalism
#### Creating a Lovelace Card
**Purpose**: Cards allow users to tell different stories about their house (based on gallery)
@@ -462,17 +424,6 @@ this.hass.localize("ui.panel.config.updates.update_available", {
- Use HTTPS - All external resources must use HTTPS
- CSP compliance - Ensure code works with Content Security Policy
### Pull Requests
When creating a pull request, you **must** use the PR template located at `.github/PULL_REQUEST_TEMPLATE.md`. Read the template file and use its full content as the PR body, filling in each section appropriately.
- Do not omit, reorder, or rewrite the template sections
- Check the appropriate "Type of change" box based on the changes
- Do not check the checklist items on behalf of the user — those are the user's responsibility to review and check
- If the PR includes UI changes, remind the user to add screenshots or a short video to the PR after creating it
- Be simple and user friendly — explain what the change does, not implementation details
- Use markdown so the user can copy it
### Text and Copy Guidelines
#### Terminology Standards
@@ -535,6 +486,7 @@ When creating a pull request, you **must** use the PR template located at `.gith
#### Key Terminology
- **"add-on"** (hyphenated, not "addon")
- **"integration"** (preferred over "component")
- **Technical terms**: Use lowercase (automation, entity, device, service)
@@ -626,10 +578,13 @@ this.hass.localize("ui.panel.config.automation.delete_confirm", {
- [ ] American English spelling
- [ ] Friendly, informational tone
- [ ] Avoids abbreviations and jargon
- [ ] Correct terminology (integration not component)
- [ ] Correct terminology (add-on not addon, integration not component)
### Component-Specific Checks
- [ ] Dialogs implement HassDialog interface
- [ ] Dialog styling uses haStyleDialog
- [ ] Dialog accessibility includes dialogInitialFocus
- [ ] ha-alert used correctly for messages
- [ ] ha-form uses proper schema structure
- [ ] Components handle all states (loading, error, unavailable)

View File

@@ -5,8 +5,6 @@ updates:
schedule:
interval: weekly
time: "06:00"
cooldown:
default-days: 7
open-pull-requests-limit: 10
labels:
- Dependencies

5
.github/labeler.yml vendored
View File

@@ -44,3 +44,8 @@ GitHub Actions:
- any-glob-to-any-file:
- .github/workflows/**
- .github/*.yml
Supervisor:
- changed-files:
- any-glob-to-any-file:
- hassio/src/**

View File

@@ -8,9 +8,6 @@ on:
branches:
- master
permissions:
contents: read
env:
NODE_OPTIONS: --max_old_space_size=6144
@@ -24,13 +21,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v4.2.2
with:
ref: dev
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -46,7 +42,7 @@ jobs:
- name: Deploy to Netlify
id: deploy
run: |
npx -y netlify-cli@23.7.3 deploy --dir=cast/dist --alias dev
npx -y netlify-cli deploy --dir=cast/dist --alias dev
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}
@@ -60,13 +56,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v4.2.2
with:
ref: master
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -82,7 +77,7 @@ jobs:
- name: Deploy to Netlify
id: deploy
run: |
npx -y netlify-cli@23.7.3 deploy --dir=cast/dist --prod
npx -y netlify-cli deploy --dir=cast/dist --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}

View File

@@ -18,20 +18,15 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
lint:
name: Lint and check format
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -42,7 +37,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache@v4.2.3
with:
path: |
node_modules/.cache/prettier
@@ -63,11 +58,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -83,11 +76,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -98,15 +89,32 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@v4.6.2
with:
name: frontend-bundle-stats
path: build/stats/*.json
if-no-files-found: error
- name: Upload frontend build
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
supervisor:
name: Build supervisor
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.4.0
with:
name: frontend-build
path: hass_frontend/
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install --immutable
- name: Build Application
run: ./node_modules/.bin/gulp build-hassio
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.6.2
with:
name: supervisor-bundle-stats
path: build/stats/*.json
if-no-files-found: error
retention-days: 7

View File

@@ -7,10 +7,6 @@ on:
# The branches below must be a subset of the branches above
branches: [dev]
permissions:
contents: read
security-events: write
jobs:
analyze:
name: Analyze
@@ -27,12 +23,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v4.2.2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
persist-credentials: false
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
@@ -41,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -62,4 +57,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
uses: github/codeql-action/analyze@v3

View File

@@ -9,9 +9,6 @@ on:
- dev
- master
permissions:
contents: read
env:
NODE_OPTIONS: --max_old_space_size=6144
@@ -25,13 +22,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v4.2.2
with:
ref: dev
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -47,7 +43,7 @@ jobs:
- name: Deploy to Netlify
id: deploy
run: |
npx -y netlify-cli@23.7.3 deploy --dir=demo/dist --prod
npx -y netlify-cli deploy --dir=demo/dist --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
@@ -61,13 +57,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v4.2.2
with:
ref: master
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -83,7 +78,7 @@ jobs:
- name: Deploy to Netlify
id: deploy
run: |
npx -y netlify-cli@23.7.3 deploy --dir=demo/dist --prod
npx -y netlify-cli deploy --dir=demo/dist --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }}

View File

@@ -5,9 +5,6 @@ on:
schedule:
- cron: "0 0 * * *"
permissions:
contents: read
env:
NODE_OPTIONS: --max_old_space_size=6144
@@ -19,12 +16,10 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -40,7 +35,7 @@ jobs:
- name: Deploy to Netlify
id: deploy
run: |
npx -y netlify-cli@23.7.3 deploy --dir=gallery/dist --prod
npx -y netlify-cli deploy --dir=gallery/dist --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }}

View File

@@ -10,9 +10,6 @@ on:
branches:
- dev
permissions:
contents: read
env:
NODE_OPTIONS: --max_old_space_size=6144
@@ -24,12 +21,10 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -45,7 +40,7 @@ jobs:
- name: Deploy preview to Netlify
id: deploy
run: |
npx -y netlify-cli@23.7.3 deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" \
npx -y netlify-cli deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" \
--json > deploy_output.json
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}

View File

@@ -1,6 +1,6 @@
name: "Pull Request Labeler"
on: pull_request_target # zizmor: ignore[dangerous-triggers] -- safe: only runs actions/labeler, no PR code checkout
on: pull_request_target
jobs:
triage:
@@ -10,6 +10,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Apply labels
uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
uses: actions/labeler@v5.0.0
with:
sync-labels: true

View File

@@ -5,15 +5,11 @@ on:
schedule:
- cron: "0 * * * *"
permissions:
issues: write
pull-requests: write
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
- uses: dessant/lock-threads@v5.0.1
with:
github-token: ${{ github.token }}
process-only: "issues, prs"

View File

@@ -6,7 +6,7 @@ on:
- cron: "0 1 * * *"
env:
PYTHON_VERSION: "3.14"
PYTHON_VERSION: "3.13"
NODE_OPTIONS: --max_old_space_size=6144
permissions:
@@ -20,17 +20,15 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -59,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@v4.6.2
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@v4.6.2
with:
name: translations
path: translations.tar.gz

View File

@@ -1,39 +1,25 @@
name: RelativeCI
on:
# zizmor: ignore[dangerous-triggers] -- safe: only downloads artifacts, no PR code checkout
workflow_run:
workflows: [CI]
types:
- completed
permissions:
contents: read
actions: read
jobs:
upload-frontend-modern:
name: Upload stats (frontend/modern)
upload:
name: Upload stats
if: ${{ github.event.workflow_run.conclusion == 'success' }}
strategy:
matrix:
bundle: [frontend, supervisor]
build: [modern, legacy]
runs-on: ubuntu-latest
steps:
- name: Send bundle stats and build information to RelativeCI
uses: relative-ci/agent-action@3c681926017930047fc03acaa35cd6a44efcbfc3 # v3.2.2
uses: relative-ci/agent-action@v3.0.0
with:
key: ${{ secrets.RELATIVE_CI_KEY_frontend_modern }}
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
token: ${{ github.token }}
artifactName: frontend-bundle-stats
webpackStatsFile: frontend-modern.json
upload-frontend-legacy:
name: Upload stats (frontend/legacy)
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Send bundle stats and build information to RelativeCI
uses: relative-ci/agent-action@3c681926017930047fc03acaa35cd6a44efcbfc3 # v3.2.2
with:
key: ${{ secrets.RELATIVE_CI_KEY_frontend_legacy }}
token: ${{ github.token }}
artifactName: frontend-bundle-stats
webpackStatsFile: frontend-legacy.json
artifactName: ${{ format('{0}-bundle-stats', matrix.bundle) }}
webpackStatsFile: ${{ format('{0}-{1}.json', matrix.bundle, matrix.build) }}

View File

@@ -18,6 +18,6 @@ jobs:
pull-requests: read
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7.2.0
- uses: release-drafter/release-drafter@v6.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -6,7 +6,7 @@ on:
- published
env:
PYTHON_VERSION: "3.14"
PYTHON_VERSION: "3.13"
NODE_OPTIONS: --max_old_space_size=6144
# Set default workflow permissions
@@ -19,29 +19,25 @@ jobs:
release:
name: Release
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: write # Required to upload release assets
id-token: write # For "Trusted Publisher" to PyPi
if: github.repository_owner == 'home-assistant'
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@f6f29a7ee3fa0eccadf3620a7b9ee00ab54ec03b # master
uses: home-assistant/actions/helpers/verify-version@master
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install
@@ -50,23 +46,20 @@ jobs:
run: ./script/translations_download
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Build and release package
run: |
python3 -m pip install build
python3 -m pip install twine build
export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
script/release
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
skip-existing: true
- name: Upload release assets
env:
GH_TOKEN: ${{ github.token }}
TAG_NAME: ${{ github.event.release.tag_name }}
run: gh release upload "$TAG_NAME" dist/*.whl dist/*.tar.gz --clobber
uses: softprops/action-gh-release@v2.3.2
with:
files: |
dist/*.whl
dist/*.tar.gz
wheels-init:
name: Init wheels build
@@ -74,19 +67,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Generate requirements.txt
env:
GITHUB_REF: ${{ github.ref }}
run: |
# Sleep to give pypi time to populate the new version across mirrors
sleep 240
version=$(echo "$GITHUB_REF" | awk -F"/" '{print $NF}' )
version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' )
echo "home-assistant-frontend==$version" > ./requirements.txt
# home-assistant/wheels doesn't support SHA pinning
- name: Build wheels
uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
uses: home-assistant/wheels@2025.03.0
with:
abi: cp314
abi: cp313
tag: musllinux_1_2
arch: amd64
wheels-key: ${{ secrets.WHEELS_KEY }}
@@ -100,13 +90,12 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install
- name: Download Translations
@@ -116,11 +105,37 @@ jobs:
- name: Build landing-page
run: landing-page/script/build_landing_page
- name: Tar folder
env:
TAG_NAME: ${{ github.event.release.tag_name }}
run: tar -czf "landing-page/home_assistant_frontend_landingpage-${TAG_NAME}.tar.gz" -C landing-page/dist .
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
- name: Upload release asset
uses: softprops/action-gh-release@v2.3.2
with:
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
release-supervisor:
name: Release supervisor frontend
if: github.event.release.prerelease == false
runs-on: ubuntu-latest
permissions:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install
- name: Download Translations
run: ./script/translations_download
env:
GH_TOKEN: ${{ github.token }}
TAG_NAME: ${{ github.event.release.tag_name }}
run: gh release upload "$TAG_NAME" "landing-page/home_assistant_frontend_landingpage-${TAG_NAME}.tar.gz" --clobber
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Build supervisor
run: hassio/script/build_hassio
- name: Tar folder
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
- name: Upload release asset
uses: softprops/action-gh-release@v2.3.2
with:
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz

View File

@@ -5,43 +5,14 @@ on:
issues:
types: [opened]
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number }}
jobs:
add-no-stale:
name: Add no-stale label
runs-on: ubuntu-latest
permissions:
issues: write # To add labels to issues
if: >-
github.event.issue.type.name == 'Task'
|| github.event.issue.type.name == 'Epic'
|| github.event.issue.type.name == 'Opportunity'
steps:
- name: Add no-stale label
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['no-stale']
});
check-authorization:
name: Check authorization
runs-on: ubuntu-latest
permissions:
issues: write # To comment on, label, and close issues
# Only run if this is a Task issue type (from the issue form)
if: github.event.issue.type.name == 'Task'
if: github.event.issue.issue_type == 'Task'
steps:
- name: Check if user is authorized
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v7
with:
script: |
const issueAuthor = context.payload.issue.user.login;

View File

@@ -5,17 +5,12 @@ on:
schedule:
- cron: "0 * * * *"
permissions:
actions: write
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- name: 90 days stale policy
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
uses: actions/stale@v9.1.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90

View File

@@ -1,25 +1,19 @@
name: Translations
on:
workflow_dispatch:
push:
branches:
- dev
paths:
- src/translations/en.json
permissions:
contents: read
jobs:
upload:
name: Upload
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
uses: actions/checkout@v4.2.2
- name: Upload Translations
run: |

5
.gitignore vendored
View File

@@ -15,7 +15,7 @@ dist/
!.yarn/sdks
!.yarn/versions
.pnp.*
node_modules/
/node_modules/
yarn-error.log
npm-debug.log
@@ -56,5 +56,4 @@ test/coverage/
# AI tooling
.claude
.cursor
.opencode

2
.nvmrc
View File

@@ -1 +1 @@
24.15.0
lts/iron

55
.vscode/tasks.json vendored
View File

@@ -73,6 +73,37 @@
"instanceLimit": 1
}
},
{
"label": "Develop Supervisor panel",
"type": "gulp",
"task": "develop-hassio",
"problemMatcher": {
"owner": "ha-build",
"source": "ha-build",
"fileLocation": "absolute",
"severity": "error",
"pattern": [
{
"regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)",
"severity": 1,
"file": 2,
"message": 3,
"line": 4,
"column": 5
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "Changes detected. Starting compilation",
"endsPattern": "Build done @"
}
},
"isBackground": true,
"group": "build",
"runOptions": {
"instanceLimit": 1
}
},
{
"label": "Develop Gallery",
"type": "gulp",
@@ -215,6 +246,20 @@
"instanceLimit": 1
}
},
{
"label": "Run HA Core for Supervisor in devcontainer",
"type": "shell",
"command": "SUPERVISOR=${input:supervisorHost} SUPERVISOR_TOKEN=${input:supervisorToken} script/core",
"isBackground": true,
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [],
"runOptions": {
"instanceLimit": 1
}
},
{
"label": "Setup and fetch nightly translations",
"type": "gulp",
@@ -223,6 +268,16 @@
}
],
"inputs": [
{
"id": "supervisorHost",
"type": "promptString",
"description": "The IP of the Supervisor host running the Remote API proxy add-on"
},
{
"id": "supervisorToken",
"type": "promptString",
"description": "The token for the Remote API proxy add-on"
},
{
"id": "coreUrl",
"type": "promptString",

View File

@@ -31,7 +31,7 @@ index 8795ddcaa77aea7b0356417e4bc4b19e2b3f860c..fcdc68342d9ac53936c9ed40a9ccfc2f
@@ -129,7 +129,10 @@ export async function injectManifest(
searchString: options.injectionPoint!,
});
- filesToWrite[options.swDest] = source;
+ filesToWrite[options.swDest] = source.replace(
+ url!,

File diff suppressed because one or more lines are too long

942
.yarn/releases/yarn-4.9.2.cjs vendored Executable file

File diff suppressed because one or more lines are too long

View File

@@ -1,11 +1,9 @@
compressionLevel: mixed
npmMinimalAgeGate: "3d"
defaultSemverRangePrefix: ""
enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.14.1.cjs
yarnPath: .yarn/releases/yarn-4.9.2.cjs

View File

@@ -1 +0,0 @@
.github/copilot-instructions.md

View File

@@ -1,8 +0,0 @@
# People marked here will be automatically requested for a review
# when the code that they own is touched.
# https://github.com/blog/2392-introducing-code-owners
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# Part of the frontend that mobile developper should review
src/external_app/ @bgoncal @TimoPtr
test/external_app/ @bgoncal @TimoPtr

View File

@@ -14,6 +14,7 @@ This is the repository for the official [Home Assistant](https://home-assistant.
- Development: [Instructions](https://developers.home-assistant.io/docs/frontend/development/)
- Production build: `script/build_frontend`
- Gallery: `cd gallery && script/develop_gallery`
- Supervisor: [Instructions](https://developers.home-assistant.io/docs/supervisor/developing)
## Frontend development

View File

@@ -18,14 +18,16 @@ module.exports.sourceMapURL = () => {
module.exports.ignorePackages = () => [];
// Files from NPM packages that we should replace with empty file
module.exports.emptyPackages = ({ isLandingPageBuild }) =>
module.exports.emptyPackages = ({ isHassioBuild }) =>
[
// Icons in landingpage conflict with icons in HA so we don't load.
isLandingPageBuild &&
require.resolve("@vaadin/vaadin-material-styles/typography.js"),
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
// Icons in supervisor conflict with icons in HA so we don't load.
isHassioBuild &&
require.resolve(
path.resolve(paths.root_dir, "src/components/ha-icon.ts")
),
isLandingPageBuild &&
isHassioBuild &&
require.resolve(
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
),
@@ -36,6 +38,7 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
__BUILD__: JSON.stringify(latestBuild ? "modern" : "legacy"),
__VERSION__: JSON.stringify(env.version()),
__DEMO__: false,
__SUPERVISOR__: false,
__BACKWARDS_COMPAT__: false,
__STATIC_PATH__: "/static/",
__HASS_URL__: `\`${
@@ -180,6 +183,7 @@ module.exports.babelOptions = ({
include: /\/node_modules\//,
exclude: [
"element-internals-polyfill",
"@shoelace-style",
"@?lit(?:-labs|-element|-html)?",
].map((p) => new RegExp(`/node_modules/${p}/`)),
},
@@ -288,6 +292,26 @@ module.exports.config = {
};
},
hassio({ isProdBuild, latestBuild, isStatsBuild, isTestBuild }) {
return {
name: "supervisor" + nameSuffix(latestBuild),
entry: {
entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.ts"),
},
outputPath: outputPath(paths.hassio_output_root, latestBuild),
publicPath: publicPath(latestBuild, paths.hassio_publicPath),
isProdBuild,
latestBuild,
isStatsBuild,
isTestBuild,
isHassioBuild: true,
defineOverlay: {
__SUPERVISOR__: true,
__STATIC_PATH__: `"${paths.hassio_publicPath}/static/"`,
},
};
},
gallery({ isProdBuild, latestBuild }) {
return {
name: "gallery" + nameSuffix(latestBuild),
@@ -314,7 +338,6 @@ module.exports.config = {
publicPath: publicPath(latestBuild),
isProdBuild,
latestBuild,
isLandingPageBuild: true,
};
},
};

View File

@@ -6,9 +6,9 @@ import rootConfig from "../eslint.config.mjs";
export default tseslint.config(...rootConfig, {
rules: {
"no-console": "off",
"import-x/no-extraneous-dependencies": "off",
"import-x/extensions": "off",
"import-x/no-dynamic-require": "off",
"import/no-extraneous-dependencies": "off",
"import/extensions": "off",
"import/no-dynamic-require": "off",
"global-require": "off",
"@typescript-eslint/no-require-imports": "off",
"prefer-arrow-callback": "off",

View File

@@ -24,6 +24,10 @@ gulp.task(
)
);
gulp.task("clean-hassio", async () =>
deleteSync([paths.hassio_output_root, paths.build_dir])
);
gulp.task(
"clean-gallery",
gulp.parallel("clean-translations", async () =>

View File

@@ -43,11 +43,29 @@ const compressAppModernBrotli = () =>
const compressAppModernZopfli = () =>
compressModern(paths.app_output_root, paths.app_output_latest, "zopfli");
const compressHassioModernBrotli = () =>
compressModern(
paths.hassio_output_root,
paths.hassio_output_latest,
"brotli"
);
const compressHassioModernZopfli = () =>
compressModern(
paths.hassio_output_root,
paths.hassio_output_latest,
"zopfli"
);
const compressAppOtherBrotli = () =>
compressOther(paths.app_output_root, paths.app_output_latest, "brotli");
const compressAppOtherZopfli = () =>
compressOther(paths.app_output_root, paths.app_output_latest, "zopfli");
const compressHassioOtherBrotli = () =>
compressOther(paths.hassio_output_root, paths.hassio_output_latest, "brotli");
const compressHassioOtherZopfli = () =>
compressOther(paths.hassio_output_root, paths.hassio_output_latest, "zopfli");
gulp.task(
"compress-app",
gulp.parallel(
@@ -57,3 +75,12 @@ gulp.task(
compressAppOtherZopfli
)
);
gulp.task(
"compress-hassio",
gulp.parallel(
compressHassioModernBrotli,
compressHassioOtherBrotli,
compressHassioModernZopfli,
compressHassioOtherZopfli
)
);

View File

@@ -99,44 +99,6 @@ const lokaliseProjects = {
frontend: "3420425759f6d6d241f598.13594006",
};
const POLL_INTERVAL_MS = 1000;
/* eslint-disable no-await-in-loop */
async function pollProcess(lokaliseApi, projectId, processId) {
while (true) {
const process = await lokaliseApi
.queuedProcesses()
.get(processId, { project_id: projectId });
const project =
projectId === lokaliseProjects.backend ? "backend" : "frontend";
if (process.status === "finished") {
console.log(`Lokalise export process for ${project} finished`);
return process;
}
if (process.status === "failed" || process.status === "cancelled") {
throw new Error(
`Lokalise export process for ${project} ${process.status}: ${process.message}`
);
}
console.log(
`Lokalise export process for ${project} in progress...`,
process.status,
process.details?.items_to_process
? `${Math.round(((process.details.items_processed || 0) / process.details.items_to_process) * 100)}% (${process.details.items_processed}/${process.details.items_to_process})`
: ""
);
await new Promise((resolve) => {
setTimeout(resolve, POLL_INTERVAL_MS);
});
}
}
/* eslint-enable no-await-in-loop */
gulp.task("fetch-lokalise", async function () {
let apiKey;
try {
@@ -156,60 +118,55 @@ gulp.task("fetch-lokalise", async function () {
]);
await Promise.all(
Object.entries(lokaliseProjects).map(async ([project, projectId]) => {
try {
const exportProcess = await lokaliseApi
.files()
.async_download(projectId, {
format: "json",
original_filenames: false,
replace_breaks: false,
json_unescaped_slashes: true,
export_empty_as: "skip",
filter_data: ["verified"],
});
const finishedProcess = await pollProcess(
lokaliseApi,
projectId,
exportProcess.process_id
);
const bundleUrl = finishedProcess.details.download_url;
console.log(`Downloading translations from: ${bundleUrl}`);
const response = await fetch(bundleUrl);
if (response.status !== 200 && response.status !== 0) {
Object.entries(lokaliseProjects).map(([project, projectId]) =>
lokaliseApi
.files()
.download(projectId, {
format: "json",
original_filenames: false,
replace_breaks: false,
json_unescaped_slashes: true,
export_empty_as: "skip",
filter_data: ["verified"],
})
.then((download) => fetch(download.bundle_url))
.then((response) => {
if (response.status === 200 || response.status === 0) {
return response.arrayBuffer();
}
throw new Error(response.statusText);
}
console.log(`Extracting translations...`);
const contents = await JSZip.loadAsync(await response.arrayBuffer());
await mkdirPromise;
await Promise.all(
Object.keys(contents.files).map(async (filename) => {
const file = contents.file(filename);
if (!file) {
// no file, probably a directory
return;
}
const content = await file.async("nodebuffer");
await fs.writeFile(
path.join(inDir, project, filename.split("/").splice(-1)[0]),
content,
{ flag: "w", encoding }
);
})
);
} catch (err) {
console.error(err);
throw err;
}
})
})
.then(JSZip.loadAsync)
.then(async (contents) => {
await mkdirPromise;
return Promise.all(
Object.keys(contents.files).map(async (filename) => {
const file = contents.file(filename);
if (!file) {
// no file, probably a directory
return Promise.resolve();
}
return file
.async("nodebuffer")
.then((content) =>
fs.writeFile(
path.join(
inDir,
project,
filename.split("/").splice(-1)[0]
),
content,
{ flag: "w", encoding }
)
);
})
);
})
.catch((err) => {
console.error(err);
throw err;
})
)
);
});

View File

@@ -266,3 +266,28 @@ gulp.task(
paths.landingPage_output_es5
)
);
const HASSIO_PAGE_ENTRIES = { "entrypoint.js": ["entrypoint"] };
gulp.task(
"gen-pages-hassio-dev",
genPagesDevTask(
HASSIO_PAGE_ENTRIES,
paths.hassio_dir,
paths.hassio_output_root,
"src",
paths.hassio_publicPath
)
);
gulp.task(
"gen-pages-hassio-prod",
genPagesProdTask(
HASSIO_PAGE_ENTRIES,
paths.hassio_dir,
paths.hassio_output_root,
paths.hassio_output_latest,
paths.hassio_output_es5,
"src"
)
);

View File

@@ -123,11 +123,22 @@ gulp.task("copy-translations-app", async () => {
copyTranslations(staticDir);
});
gulp.task("copy-translations-supervisor", async () => {
const staticDir = paths.hassio_output_static;
copyTranslations(staticDir);
});
gulp.task("copy-translations-landing-page", async () => {
const staticDir = paths.landingPage_output_static;
copyTranslations(staticDir);
});
gulp.task("copy-static-supervisor", async () => {
const staticDir = paths.hassio_output_static;
copyLocaleData(staticDir);
copyFonts(staticDir);
});
gulp.task("copy-static-app", async () => {
const staticDir = paths.app_output_static;
// Basic static files

View File

@@ -0,0 +1,45 @@
import gulp from "gulp";
import env from "../env.cjs";
import "./clean.js";
import "./compress.js";
import "./entry-html.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./translations.js";
import "./rspack.js";
gulp.task(
"develop-hassio",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "development";
},
"clean-hassio",
"gen-dummy-icons-json",
"gen-pages-hassio-dev",
"build-supervisor-translations",
"copy-translations-supervisor",
"build-locale-data",
"copy-static-supervisor",
"rspack-watch-hassio"
)
);
gulp.task(
"build-hassio",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
"clean-hassio",
"gen-dummy-icons-json",
"build-supervisor-translations",
"copy-translations-supervisor",
"build-locale-data",
"copy-static-supervisor",
"rspack-prod-hassio",
"gen-pages-hassio-prod",
...// Don't compress running tests
(env.isTestBuild() ? [] : ["compress-hassio"])
)
);

View File

@@ -9,6 +9,7 @@ import "./fetch-nightly-translations.js";
import "./gallery.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./hassio.js";
import "./landing-page.js";
import "./locale-data.js";
import "./rspack.js";

View File

@@ -40,24 +40,18 @@ const convertToJSON = async (
throw e;
}
// Convert to JSON
const parts = localeData.split("} else {");
const firstBlock = parts[0];
const obj = INTL_POLYFILLS[pkg];
const dataRegex = new RegExp(
`Intl\\.${obj}\\.${addFunc}\\((?<data>.*)\\)`,
"s"
);
localeData = firstBlock.match(dataRegex)?.groups?.data;
localeData = localeData.match(dataRegex)?.groups?.data;
if (!localeData) {
throw Error(`Failed to extract data for language ${lang} from ${pkg}`);
}
// Parse to validate JSON, then stringify to minify
try {
localeData = JSON.stringify(JSON.parse(localeData));
await writeFile(join(outDir, `${pkg}/${lang}.json`), localeData);
} catch (e) {
throw Error(`Failed to parse JSON for language ${lang} from ${pkg}: ${e}`);
}
localeData = JSON.stringify(JSON.parse(localeData));
await writeFile(join(outDir, `${pkg}/${lang}.json`), localeData);
};
gulp.task("clean-locale-data", async () => deleteSync([outDir]));

View File

@@ -13,6 +13,7 @@ import {
createCastConfig,
createDemoConfig,
createGalleryConfig,
createHassioConfig,
createLandingPageConfig,
} from "../rspack.cjs";
@@ -57,12 +58,6 @@ const runDevServer = async ({
directory: contentBase,
watch: true,
},
client: {
overlay: {
runtimeErrors: (error) =>
!error?.message?.includes("ResizeObserver loop"),
},
},
proxy,
},
compiler
@@ -164,6 +159,31 @@ gulp.task("rspack-prod-cast", () =>
)
);
gulp.task("rspack-watch-hassio", () => {
// This command will run forever because we don't close compiler
rspack(
createHassioConfig({
isProdBuild: false,
latestBuild: true,
})
).watch({ ignored: /build/, poll: isWsl }, doneHandler());
gulp.watch(
path.join(paths.translations_src, "en.json"),
gulp.series("build-supervisor-translations", "copy-translations-supervisor")
);
});
gulp.task("rspack-prod-hassio", () =>
prodBuild(
bothBuilds(createHassioConfig, {
isProdBuild: true,
isStatsBuild: env.isStatsBuild(),
isTestBuild: env.isTestBuild(),
})
)
);
gulp.task("rspack-dev-server-gallery", () =>
runDevServer({
compiler: rspack(

View File

@@ -156,9 +156,7 @@ const createTestTranslation = () =>
*/
const createMasterTranslation = () =>
gulp
.src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])], {
allowEmpty: true,
})
.src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])])
.pipe(new CustomJSON(lokaliseTransform))
.pipe(new MergeJSON("en"))
.pipe(gulp.dest(workDir));
@@ -170,7 +168,9 @@ const setFragment = (fragment) => async () => {
};
const panelFragment = (fragment) =>
fragment !== "base" && fragment !== "landing-page";
fragment !== "base" &&
fragment !== "supervisor" &&
fragment !== "landing-page";
const HASHES = new Map();
@@ -205,15 +205,18 @@ const createTranslations = async () => {
FRAGMENTS.map((fragment) => {
switch (fragment) {
case "base":
// Remove the panels and landing-page to create the base translations
// Remove the panels and supervisor to create the base translations
return [
flatten({
...data,
ui: { ...data.ui, panel: undefined },
"landing-page": undefined,
supervisor: undefined,
}),
"",
];
case "supervisor":
// Supervisor key is at the top level
return [flatten(data.supervisor), ""];
case "landing-page":
// landing-page key is at the top level
return [flatten(data["landing-page"]), ""];
@@ -313,6 +316,11 @@ gulp.task(
)
);
gulp.task(
"build-supervisor-translations",
gulp.series(setFragment("supervisor"), "build-translations")
);
gulp.task(
"build-landing-page-translations",
gulp.series(setFragment("landing-page"), "build-translations")

View File

@@ -6,6 +6,7 @@ import presetEnv from "@babel/preset-env";
import compilationTargets from "@babel/helper-compilation-targets";
import coreJSCompat from "core-js-compat";
import { logPlugin } from "@babel/preset-env/lib/debug.js";
// eslint-disable-next-line import/no-relative-packages
import shippedPolyfills from "../node_modules/babel-plugin-polyfill-corejs3/lib/shipped-proposals.js";
import { babelOptions } from "./bundle.cjs";

View File

@@ -49,5 +49,15 @@ module.exports = {
"../landing-page/dist/static"
),
hassio_dir: path.resolve(__dirname, "../hassio"),
hassio_output_root: path.resolve(__dirname, "../hassio/build"),
hassio_output_static: path.resolve(__dirname, "../hassio/build/static"),
hassio_output_latest: path.resolve(
__dirname,
"../hassio/build/frontend_latest"
),
hassio_output_es5: path.resolve(__dirname, "../hassio/build/frontend_es5"),
hassio_publicPath: "/api/hassio/app",
translations_src: path.resolve(__dirname, "../src/translations"),
};

View File

@@ -40,7 +40,7 @@ const createRspackConfig = ({
latestBuild,
isStatsBuild,
isTestBuild,
isLandingPageBuild,
isHassioBuild,
dontHash,
}) => {
if (!dontHash) {
@@ -167,12 +167,10 @@ const createRspackConfig = ({
);
},
}),
bundle.emptyPackages({ isLandingPageBuild }).length
? new rspack.NormalModuleReplacementPlugin(
new RegExp(bundle.emptyPackages({ isLandingPageBuild }).join("|")),
path.resolve(paths.root_dir, "src/util/empty.js")
)
: false,
new rspack.NormalModuleReplacementPlugin(
new RegExp(bundle.emptyPackages({ isHassioBuild }).join("|")),
path.resolve(paths.root_dir, "src/util/empty.js")
),
!isProdBuild && new LogStartCompilePlugin(),
isProdBuild &&
new StatsWriterPlugin({
@@ -200,7 +198,6 @@ const createRspackConfig = ({
"lit/decorators$": "lit/decorators.js",
"lit/directive$": "lit/directive.js",
"lit/directives/until$": "lit/directives/until.js",
"lit/directives/ref$": "lit/directives/ref.js",
"lit/directives/class-map$": "lit/directives/class-map.js",
"lit/directives/style-map$": "lit/directives/style-map.js",
"lit/directives/if-defined$": "lit/directives/if-defined.js",
@@ -209,9 +206,7 @@ const createRspackConfig = ({
"lit/directives/join$": "lit/directives/join.js",
"lit/directives/repeat$": "lit/directives/repeat.js",
"lit/directives/live$": "lit/directives/live.js",
"lit/directives/keyed$": latestBuild
? "lit/directives/keyed.js"
: path.resolve(__dirname, "../src/common/lit/keyed-es5.ts"),
"lit/directives/keyed$": "lit/directives/keyed.js",
"lit/polyfill-support$": "lit/polyfill-support.js",
"@lit-labs/virtualizer/layouts/grid":
"@lit-labs/virtualizer/layouts/grid.js",
@@ -219,42 +214,6 @@ const createRspackConfig = ({
"@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$":
"@formatjs/intl-durationformat/should-polyfill.js",
"@formatjs/intl-durationformat/polyfill-force$":
"@formatjs/intl-durationformat/polyfill-force.js",
"@formatjs/intl-datetimeformat/should-polyfill":
"@formatjs/intl-datetimeformat/should-polyfill.js",
"@formatjs/intl-datetimeformat/polyfill-force":
"@formatjs/intl-datetimeformat/polyfill-force.js",
"@formatjs/intl-displaynames/should-polyfill":
"@formatjs/intl-displaynames/should-polyfill.js",
"@formatjs/intl-displaynames/polyfill-force":
"@formatjs/intl-displaynames/polyfill-force.js",
"@formatjs/intl-getcanonicallocales/should-polyfill":
"@formatjs/intl-getcanonicallocales/should-polyfill.js",
"@formatjs/intl-getcanonicallocales/polyfill-force":
"@formatjs/intl-getcanonicallocales/polyfill-force.js",
"@formatjs/intl-listformat/should-polyfill":
"@formatjs/intl-listformat/should-polyfill.js",
"@formatjs/intl-listformat/polyfill-force":
"@formatjs/intl-listformat/polyfill-force.js",
"@formatjs/intl-locale/should-polyfill":
"@formatjs/intl-locale/should-polyfill.js",
"@formatjs/intl-locale/polyfill-force":
"@formatjs/intl-locale/polyfill-force.js",
"@formatjs/intl-numberformat/should-polyfill":
"@formatjs/intl-numberformat/should-polyfill.js",
"@formatjs/intl-numberformat/polyfill-force":
"@formatjs/intl-numberformat/polyfill-force.js",
"@formatjs/intl-pluralrules/should-polyfill":
"@formatjs/intl-pluralrules/should-polyfill.js",
"@formatjs/intl-pluralrules/polyfill-force":
"@formatjs/intl-pluralrules/polyfill-force.js",
"@formatjs/intl-relativetimeformat/should-polyfill":
"@formatjs/intl-relativetimeformat/should-polyfill.js",
"@formatjs/intl-relativetimeformat/polyfill-force":
"@formatjs/intl-relativetimeformat/polyfill-force.js",
},
},
output: {
@@ -298,6 +257,7 @@ const createRspackConfig = ({
),
},
experiments: {
layers: true,
outputModule: true,
},
};
@@ -321,6 +281,21 @@ const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
const createCastConfig = ({ isProdBuild, latestBuild }) =>
createRspackConfig(bundle.config.cast({ isProdBuild, latestBuild }));
const createHassioConfig = ({
isProdBuild,
latestBuild,
isStatsBuild,
isTestBuild,
}) =>
createRspackConfig(
bundle.config.hassio({
isProdBuild,
latestBuild,
isStatsBuild,
isTestBuild,
})
);
const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
createRspackConfig(bundle.config.gallery({ isProdBuild, latestBuild }));
@@ -331,6 +306,7 @@ module.exports = {
createAppConfig,
createDemoConfig,
createCastConfig,
createHassioConfig,
createGalleryConfig,
createRspackConfig,
createLandingPageConfig,

View File

@@ -14,5 +14,5 @@
"name": "Home Assistant Cast",
"short_name": "HA Cast",
"start_url": "/?homescreen=1",
"theme_color": "#009ac7"
"theme_color": "#03A9F4"
}

View File

@@ -1,7 +1,9 @@
import "@material/mwc-button/mwc-button";
import type { ActionDetail } from "@material/mwc-list/mwc-list";
import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js";
import type { Auth, Connection } from "home-assistant-js-websocket";
import type { TemplateResult, PropertyValues } from "lit";
import type { TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { CastManager } from "../../../../src/cast/cast_manager";
@@ -16,7 +18,6 @@ import {
} from "../../../../src/common/auth/token_storage";
import { atLeastVersion } from "../../../../src/common/config/version";
import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-icon";
import "../../../../src/components/ha-list";
import "../../../../src/components/ha-list-item";
@@ -28,6 +29,7 @@ import {
import { isStrategyDashboard } from "../../../../src/data/lovelace/config/types";
import type { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view";
import "../../../../src/layouts/hass-loading-screen";
import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
import "./hc-layout";
@customElement("hc-cast")
@@ -61,20 +63,12 @@ class HcCast extends LitElement {
<p class="question action-item">
Stay logged in?
<span>
<ha-button
appearance="plain"
size="small"
@click=${this._handleSaveTokens}
>
<mwc-button @click=${this._handleSaveTokens}>
YES
</ha-button>
<ha-button
appearance="plain"
size="small"
@click=${this._handleSkipSaveTokens}
>
</mwc-button>
<mwc-button @click=${this._handleSkipSaveTokens}>
NO
</ha-button>
</mwc-button>
</span>
</p>
`
@@ -84,10 +78,10 @@ class HcCast extends LitElement {
: !this.castManager.status
? html`
<p class="center-item">
<ha-button @click=${this._handleLaunch}>
<ha-svg-icon slot="start" .path=${mdiCast}></ha-svg-icon>
<mwc-button raised @click=${this._handleLaunch}>
<ha-svg-icon .path=${mdiCast}></ha-svg-icon>
Start Casting
</ha-button>
</mwc-button>
</p>
`
: html`
@@ -95,9 +89,7 @@ class HcCast extends LitElement {
<ha-list @action=${this._handlePickView} activatable>
${(
this.lovelaceViews ?? [
{
title: "Home",
},
generateDefaultViewConfig({}, {}, {}, {}, () => ""),
]
).map(
(view, idx) => html`
@@ -129,28 +121,20 @@ class HcCast extends LitElement {
<div class="card-actions">
${this.castManager.status
? html`
<ha-button appearance="plain" @click=${this._handleLaunch}>
<ha-svg-icon
slot="start"
.path=${mdiCastConnected}
></ha-svg-icon>
<mwc-button @click=${this._handleLaunch}>
<ha-svg-icon .path=${mdiCastConnected}></ha-svg-icon>
Manage
</ha-button>
</mwc-button>
`
: ""}
<div class="spacer"></div>
<ha-button
variant="danger"
appearance="plain"
@click=${this._handleLogout}
>Log out</ha-button
>
<mwc-button @click=${this._handleLogout}>Log out</mwc-button>
</div>
</hc-layout>
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const llColl = atLeastVersion(this.connection.haVersion, 0, 107)
@@ -183,7 +167,7 @@ class HcCast extends LitElement {
});
}
protected updated(changedProps: PropertyValues<this>) {
protected updated(changedProps) {
super.updated(changedProps);
toggleAttribute(
this,
@@ -206,7 +190,7 @@ class HcCast extends LitElement {
}
private async _handlePickView(ev: CustomEvent<ActionDetail>) {
const path = this.lovelaceViews?.[ev.detail.index]?.path ?? ev.detail.index;
const path = this.lovelaceViews![ev.detail.index].path ?? ev.detail.index;
await ensureConnectedCastSession(this.castManager!, this.auth!);
castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path);
}
@@ -243,7 +227,7 @@ class HcCast extends LitElement {
}
.question:before {
border-radius: var(--ha-border-radius-sm);
border-radius: 4px;
position: absolute;
top: 0;
right: 0;
@@ -261,6 +245,13 @@ class HcCast extends LitElement {
color: var(--secondary-text-color);
}
mwc-button ha-svg-icon {
margin-right: 8px;
margin-inline-end: 8px;
margin-inline-start: initial;
height: 18px;
}
ha-list-item ha-icon,
ha-list-item ha-svg-icon {
padding: 12px;

View File

@@ -1,3 +1,4 @@
import "@material/mwc-button";
import { mdiCastConnected, mdiCast } from "@mdi/js";
import type {
Auth,
@@ -12,7 +13,7 @@ import {
ERR_INVALID_HTTPS_TO_HTTP,
getAuth,
} from "home-assistant-js-websocket";
import type { TemplateResult, PropertyValues } from "lit";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import type { CastManager } from "../../../../src/cast/cast_manager";
@@ -26,8 +27,7 @@ import "../../../../src/components/ha-svg-icon";
import "../../../../src/layouts/hass-loading-screen";
import { registerServiceWorker } from "../../../../src/util/register-service-worker";
import "./hc-layout";
import "../../../../src/components/input/ha-input";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-textfield";
const seeFAQ = (qid) => html`
See <a href="./faq.html${qid ? `#${qid}` : ""}">the FAQ</a> for more
@@ -83,14 +83,11 @@ export class HcConnect extends LitElement {
Unable to connect to ${tokens!.hassUrl}.
</div>
<div class="card-actions">
<ha-button appearance="plain" href="/">Retry</ha-button>
<a href="/">
<mwc-button> Retry </mwc-button>
</a>
<div class="spacer"></div>
<ha-button
appearance="plain"
variant="danger"
@click=${this._handleLogout}
>Log out</ha-button
>
<mwc-button @click=${this._handleLogout}>Log out</mwc-button>
</div>
</hc-layout>
`;
@@ -123,27 +120,24 @@ export class HcConnect extends LitElement {
To get started, enter your Home Assistant URL and click authorize.
If you want a preview instead, click the show demo button.
</p>
<ha-input
<ha-textfield
label="Home Assistant URL"
placeholder="https://abcdefghijklmnop.ui.nabu.casa"
@keydown=${this._handleInputKeyDown}
></ha-input>
></ha-textfield>
${this.error ? html` <p class="error">${this.error}</p> ` : ""}
</div>
<div class="card-actions">
<ha-button appearance="plain" @click=${this._handleDemo}>
<mwc-button @click=${this._handleDemo}>
Show Demo
<ha-svg-icon
slot="end"
.path=${this.castManager.castState === "CONNECTED"
? mdiCastConnected
: mdiCast}
></ha-svg-icon>
</ha-button>
</mwc-button>
<div class="spacer"></div>
<ha-button appearance="plain" @click=${this._handleConnect}
>Authorize</ha-button
>
<mwc-button @click=${this._handleConnect}>Authorize</mwc-button>
</div>
</hc-layout>
`;
@@ -158,7 +152,7 @@ export class HcConnect extends LitElement {
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
import("./hc-cast");
@@ -204,7 +198,7 @@ export class HcConnect extends LitElement {
}
private async _handleConnect() {
const inputEl = this.shadowRoot!.querySelector("ha-input")!;
const inputEl = this.shadowRoot!.querySelector("ha-textfield")!;
const value = inputEl.value || "";
this.error = undefined;
@@ -315,11 +309,15 @@ export class HcConnect extends LitElement {
color: darkred;
}
mwc-button ha-svg-icon {
margin-left: 8px;
}
.spacer {
flex: 1;
}
ha-input {
ha-textfield {
width: 100%;
}
`;

View File

@@ -1,6 +1,6 @@
import type { Auth, Connection, HassUser } from "home-assistant-js-websocket";
import { getUser } from "home-assistant-js-websocket";
import type { TemplateResult, PropertyValues } from "lit";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../src/components/ha-card";
@@ -53,7 +53,7 @@ class HcLayout extends LitElement {
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
if (this.connection) {
@@ -95,8 +95,7 @@ class HcLayout extends LitElement {
}
.hero {
border-radius: var(--ha-border-radius-sm) var(--ha-border-radius-sm)
var(--ha-border-radius-square) var(--ha-border-radius-square);
border-radius: 4px 4px 0 0;
}
.subtitle {
font-size: var(--ha-font-size-m);

View File

@@ -5,19 +5,17 @@ const castContext = framework.CastReceiverContext.getInstance();
const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor(
"LOAD" as framework.messages.MessageType.LOAD,
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.streamType = 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;
framework.messages.HlsVideoSegmentFormat.FMP4;
}
return loadRequestData;
}

View File

@@ -1,7 +1,8 @@
import type { EntityInput } from "../../../../src/fake_data/entities/types";
import type { Entity } from "../../../../src/fake_data/entity";
import { convertEntities } from "../../../../src/fake_data/entity";
export const castDemoEntities: () => EntityInput[] = () =>
Object.values({
export const castDemoEntities: () => Entity[] = () =>
convertEntities({
"light.reading_light": {
entity_id: "light.reading_light",
state: "on",
@@ -74,7 +75,7 @@ export const castDemoEntities: () => EntityInput[] = () =>
longitude: 4.8903147,
radius: 100,
friendly_name: "Home",
icon: "mdi:home",
icon: "hass:home",
},
},
"input_number.harmonyvolume": {
@@ -87,7 +88,7 @@ export const castDemoEntities: () => EntityInput[] = () =>
step: 1,
mode: "slider",
friendly_name: "Volume",
icon: "mdi:volume-high",
icon: "hass:volume-high",
},
},
"climate.upstairs": {

View File

@@ -56,7 +56,7 @@ export const castDemoLovelace: () => LovelaceConfig = () => {
type: "weblink",
url: "/lovelace/climate",
name: "Climate controls",
icon: "mdi:arrow-right",
icon: "hass:arrow-right",
},
],
},
@@ -76,7 +76,7 @@ export const castDemoLovelace: () => LovelaceConfig = () => {
type: "weblink",
url: "/lovelace/overview",
name: "Back",
icon: "mdi:arrow-left",
icon: "hass:arrow-left",
},
],
},

View File

@@ -1,9 +1,10 @@
import { framework } from "./cast_framework";
import { CAST_NS } from "../../../src/cast/const";
import type { HassMessage } from "../../../src/cast/receiver_messages";
import "../../../src/resources/custom-card-support";
import { castContext } from "./cast_context";
import { framework } from "./cast_framework";
import { HcMain } from "./layout/hc-main";
import type { ReceivedMessage } from "./types";
const lovelaceController = new HcMain();
document.body.append(lovelaceController);
@@ -39,8 +40,7 @@ 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" as framework.messages.StreamType.NONE;
loadRequestData.media.streamType = framework.messages.StreamType.NONE;
const metadata = new framework.messages.GenericMediaMetadata();
metadata.title = viewTitle;
loadRequestData.media.metadata = metadata;
@@ -89,30 +89,31 @@ const showMediaPlayer = () => {
const options = new framework.CastReceiverOptions();
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" as framework.system.MessageType.JSON,
[CAST_NS]: 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" as framework.messages.PlayerState.IDLE)
) {
playerManager.stop();
} else {
showLovelaceController();
castContext.addCustomMessageListener(
CAST_NS,
// @ts-ignore
(ev: ReceivedMessage<HassMessage>) => {
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
if (
playerManager.getPlayerState() !== framework.messages.PlayerState.IDLE
) {
playerManager.stop();
} else {
showLovelaceController();
}
const msg = ev.data;
msg.senderId = ev.senderId;
lovelaceController.processIncomingMessage(msg);
}
const msg = ev.data as HassMessage;
msg.senderId = ev.senderId;
lovelaceController.processIncomingMessage(msg);
});
);
const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor(
"LOAD" as framework.messages.MessageType.LOAD,
framework.messages.MessageType.LOAD,
(loadRequestData) => {
if (
loadRequestData.media.contentId ===
@@ -126,26 +127,24 @@ playerManager.setMessageInterceptor(
// Special handling if it came from Google Assistant
if (media.entity) {
media.contentId = media.entity;
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
media.streamType = 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
// @ts-ignore
media.hlsVideoSegmentFormat =
"FMP4" as framework.messages.HlsVideoSegmentFormat.FMP4;
framework.messages.HlsVideoSegmentFormat.FMP4;
}
return loadRequestData;
}
);
playerManager.addEventListener(
"MEDIA_STATUS" as framework.events.EventType.MEDIA_STATUS,
framework.events.EventType.MEDIA_STATUS,
(event) => {
if (
event.mediaStatus?.playerState ===
("IDLE" as framework.messages.PlayerState.IDLE) &&
event.mediaStatus?.playerState === framework.messages.PlayerState.IDLE &&
event.mediaStatus?.idleReason &&
event.mediaStatus?.idleReason !==
("INTERRUPTED" as framework.messages.IdleReason.INTERRUPTED)
framework.messages.IdleReason.INTERRUPTED
) {
// media finished or stopped, return to default Lovelace
showLovelaceController();

View File

@@ -1,4 +1,3 @@
import type { PropertyValues } from "lit";
import { html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { mockHistory } from "../../../../demo/src/stubs/history";
@@ -30,7 +29,7 @@ class HcDemo extends HassElement {
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._initializeHass();
}

View File

@@ -1,5 +1,4 @@
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, type TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
@@ -65,7 +64,7 @@ class HcLovelace extends LitElement {
`;
}
protected updated(changedProps: PropertyValues<this>) {
protected updated(changedProps) {
super.updated(changedProps);
if (changedProps.has("viewPath") || changedProps.has("lovelaceConfig")) {

View File

@@ -1,6 +1,6 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { createConnection, getAuth } from "home-assistant-js-websocket";
import type { TemplateResult, PropertyValues } from "lit";
import type { TemplateResult } from "lit";
import { html } from "lit";
import { customElement, state } from "lit/decorators";
import { CAST_NS } from "../../../../src/cast/const";
@@ -106,7 +106,7 @@ export class HcMain extends HassElement {
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
import("./hc-lovelace");
import("../../../../src/resources/append-ha-style");
@@ -305,8 +305,9 @@ export class HcMain extends HassElement {
await llColl.refresh();
this._unsubLovelace = llColl.subscribe(async (rawConfig) => {
if (isStrategyDashboard(rawConfig)) {
const { generateLovelaceDashboardStrategy } =
await import("../../../../src/panels/lovelace/strategies/get-strategy");
const { generateLovelaceDashboardStrategy } = await import(
"../../../../src/panels/lovelace/strategies/get-strategy"
);
const config = await generateLovelaceDashboardStrategy(
rawConfig,
this.hass!
@@ -346,8 +347,9 @@ export class HcMain extends HassElement {
}
private async _generateDefaultLovelaceConfig() {
const { generateLovelaceDashboardStrategy } =
await import("../../../../src/panels/lovelace/strategies/get-strategy");
const { generateLovelaceDashboardStrategy } = await import(
"../../../../src/panels/lovelace/strategies/get-strategy"
);
this._handleNewLovelaceConfig(
await generateLovelaceDashboardStrategy(DEFAULT_CONFIG, this.hass!)
);

View File

@@ -0,0 +1,6 @@
export interface ReceivedMessage<T> {
gj: boolean;
data: T;
senderId: string;
type: "message";
}

View File

@@ -75,5 +75,5 @@
"name": "Home Assistant Demo",
"short_name": "HA Demo",
"start_url": "/?homescreen=1",
"theme_color": "#009ac7"
"theme_color": "#03A9F4"
}

View File

@@ -1,7 +1,8 @@
import { convertEntities } from "../../../../src/fake_data/entity";
import type { DemoConfig } from "../types";
export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
Object.values({
convertEntities({
"todo.shopping_list": {
entity_id: "todo.shopping_list",
state: "2",
@@ -142,7 +143,7 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
state: "on",
attributes: {
friendly_name: "Home Automation",
icon: "mdi:home-automation",
icon: "hass:home-automation",
},
},
"input_boolean.tvtime": {

View File

@@ -4,7 +4,7 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
title: "Home Assistant",
views: [
{
icon: "mdi:home-assistant",
icon: "hass:home-assistant",
id: "home",
title: "Home",
cards: [

View File

@@ -11,9 +11,9 @@ export const demoConfigs: (() => Promise<DemoConfig>)[] = [
() => import("./jimpower").then((mod) => mod.demoJimpower),
];
// eslint-disable-next-line import-x/no-mutable-exports
// eslint-disable-next-line import/no-mutable-exports
export let selectedDemoConfigIndex = 0;
// eslint-disable-next-line import-x/no-mutable-exports
// eslint-disable-next-line import/no-mutable-exports
export let selectedDemoConfig: Promise<DemoConfig> =
demoConfigs[selectedDemoConfigIndex]();

View File

@@ -1,7 +1,8 @@
import { convertEntities } from "../../../../src/fake_data/entity";
import type { DemoConfig } from "../types";
export const demoEntitiesJimpower: DemoConfig["entities"] = () =>
Object.values({
convertEntities({
"todo.shopping_list": {
entity_id: "todo.shopping_list",
state: "2",

View File

@@ -1236,7 +1236,7 @@ export const demoLovelaceJimpower: DemoConfig["lovelace"] = () => ({
},
],
path: "security",
icon: "mdi:shield-home",
icon: "hass:shield-home",
name: "Security",
background:
'center / cover no-repeat url("/assets/jimpower/background-15.jpg") fixed',

View File

@@ -1,7 +1,8 @@
import { convertEntities } from "../../../../src/fake_data/entity";
import type { DemoConfig } from "../types";
export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
Object.values({
convertEntities({
"todo.shopping_list": {
entity_id: "todo.shopping_list",
state: "2",

View File

@@ -1,7 +1,8 @@
import { convertEntities } from "../../../../src/fake_data/entity";
import type { DemoConfig } from "../types";
export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
Object.values({
convertEntities({
"cover.living_room_garden_shutter": {
entity_id: "cover.living_room_garden_shutter",
state: "open",
@@ -141,7 +142,7 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
},
},
"device_tracker.car": {
entity_id: "device_tracker.car",
entity_id: "sensor.outdoor_humidity",
state: "not_home",
attributes: {
friendly_name: "Car",
@@ -199,7 +200,7 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
},
},
"binary_sensor.kitchen_motion": {
entity_id: "binary_sensor.kitchen_motion",
entity_id: "light.kitchen_motion",
state: "on",
attributes: {
device_class: "motion",
@@ -335,7 +336,7 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
},
},
"sensor.rain": {
entity_id: "sensor.rain",
entity_id: "sensor.moon_phase",
state: "7.2",
attributes: {
state_class: "total_increasing",
@@ -565,7 +566,7 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
},
},
"update.home_assistant_core_update": {
entity_id: "update.home_assistant_core_update",
entity_id: "update.home_assistant_supervisor_update",
state: "off",
attributes: {
auto_update: false,

View File

@@ -1,7 +1,8 @@
import { convertEntities } from "../../../../src/fake_data/entity";
import type { DemoConfig } from "../types";
export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
Object.values({
convertEntities({
"todo.shopping_list": {
entity_id: "todo.shopping_list",
state: "2",

View File

@@ -1,7 +1,7 @@
import type { TemplateResult } from "lit";
import type { LocalizeFunc } from "../../../src/common/translations/localize";
import type { LovelaceConfig } from "../../../src/data/lovelace/config/types";
import type { EntityInput } from "../../../src/fake_data/entities/types";
import type { Entity } from "../../../src/fake_data/entity";
export interface DemoConfig {
index?: number;
@@ -12,6 +12,6 @@ export interface DemoConfig {
| string
| ((localize: LocalizeFunc) => string | TemplateResult<1>);
lovelace: (localize: LocalizeFunc) => LovelaceConfig;
entities: (localize: LocalizeFunc) => EntityInput[];
entities: (localize: LocalizeFunc) => Entity[];
theme: () => Record<string, string> | null;
}

View File

@@ -1,6 +1,4 @@
/// <reference types="chromecast-caf-sender" />
import { mdiTelevision } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import type { CastManager } from "../../../src/cast/cast_manager";
@@ -38,7 +36,7 @@ class CastDemoRow extends LitElement implements LovelaceRow {
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
import("../../../src/cast/cast_manager").then(({ getCastManager }) =>
getCastManager().then((mgr) => {
@@ -63,7 +61,7 @@ class CastDemoRow extends LitElement implements LovelaceRow {
);
}
protected updated(changedProps: PropertyValues<this>) {
protected updated(changedProps) {
super.updated(changedProps);
this.style.display = this._castManager ? "" : "none";
}

View File

@@ -1,4 +1,4 @@
import type { CSSResultGroup, PropertyValues } from "lit";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
@@ -89,20 +89,17 @@ export class HADemoCard extends LitElement implements LovelaceCard {
)}
</div>
<div class="actions small-hidden">
<ha-button
appearance="plain"
size="small"
href="https://www.home-assistant.io"
target="_blank"
>
${this.hass.localize("ui.panel.page-demo.cards.demo.learn_more")}
</ha-button>
<a href="https://www.home-assistant.io" target="_blank">
<ha-button>
${this.hass.localize("ui.panel.page-demo.cards.demo.learn_more")}
</ha-button>
</a>
</div>
</ha-card>
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
if (this._hidden) {
this.style.display = "none";

View File

@@ -9,14 +9,11 @@ import { selectedDemoConfig } from "./configs/demo-configs";
import { mockAreaRegistry } from "./stubs/area_registry";
import { mockAuth } from "./stubs/auth";
import { mockConfigEntries } from "./stubs/config_entries";
import { mockDeviceRegistry } from "./stubs/device_registry";
import { mockEnergy } from "./stubs/energy";
import { energyEntities } from "./stubs/entities";
import { mockEntityRegistry } from "./stubs/entity_registry";
import { mockEvents } from "./stubs/events";
import { mockFloorRegistry } from "./stubs/floor_registry";
import { mockFrontend } from "./stubs/frontend";
import { mockLabelRegistry } from "./stubs/label_registry";
import { mockIcons } from "./stubs/icons";
import { mockHistory } from "./stubs/history";
import { mockLovelace } from "./stubs/lovelace";
@@ -63,9 +60,6 @@ export class HaDemo extends HomeAssistantAppEl {
mockPersistentNotification(hass);
mockConfigEntries(hass);
mockAreaRegistry(hass);
mockDeviceRegistry(hass);
mockFloorRegistry(hass);
mockLabelRegistry(hass);
mockEntityRegistry(hass, [
{
config_entry_id: "co2signal",

View File

@@ -68,7 +68,7 @@
}
#ha-launch-screen .ha-launch-screen-spacer-top {
flex: 1;
margin-top: calc( 2 * max(var(--safe-area-inset-top, 0px), 48px) + 46px );
margin-top: calc( 2 * max(var(--safe-area-inset-bottom), 48px) + 46px );
padding-top: 48px;
}
#ha-launch-screen .ha-launch-screen-spacer-bottom {
@@ -76,7 +76,7 @@
padding-top: 48px;
}
.ohf-logo {
margin: max(var(--safe-area-inset-bottom, 0px), 48px) 0;
margin: max(var(--safe-area-inset-bottom), 48px) 0;
display: flex;
flex-direction: column;
align-items: center;

View File

@@ -1,4 +1,4 @@
import type { AreaRegistryEntry } from "../../../src/data/area/area_registry";
import type { AreaRegistryEntry } from "../../../src/data/area_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockAreaRegistry = (

View File

@@ -1,4 +1,4 @@
import type { DeviceRegistryEntry } from "../../../src/data/device/device_registry";
import type { DeviceRegistryEntry } from "../../../src/data/device_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockDeviceRegistry = (

View File

@@ -14,42 +14,48 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
energy_sources: [
{
type: "grid",
stat_energy_from: "sensor.energy_consumption_tarif_1",
stat_energy_to: "sensor.energy_production_tarif_1",
stat_cost: "sensor.energy_consumption_tarif_1_cost",
stat_compensation: "sensor.energy_production_tarif_1_compensation",
entity_energy_price: null,
number_energy_price: null,
entity_energy_price_export: null,
number_energy_price_export: null,
stat_rate: "sensor.power_grid",
cost_adjustment_day: 0,
},
{
type: "grid",
stat_energy_from: "sensor.energy_consumption_tarif_2",
stat_energy_to: "sensor.energy_production_tarif_2",
stat_cost: "sensor.energy_consumption_tarif_2_cost",
stat_compensation: "sensor.energy_production_tarif_2_compensation",
entity_energy_price: null,
number_energy_price: null,
entity_energy_price_export: null,
number_energy_price_export: null,
stat_rate: "sensor.power_grid_return",
flow_from: [
{
stat_energy_from: "sensor.energy_consumption_tarif_1",
stat_cost: "sensor.energy_consumption_tarif_1_cost",
entity_energy_price: null,
number_energy_price: null,
},
{
stat_energy_from: "sensor.energy_consumption_tarif_2",
stat_cost: "sensor.energy_consumption_tarif_2_cost",
entity_energy_price: null,
number_energy_price: null,
},
],
flow_to: [
{
stat_energy_to: "sensor.energy_production_tarif_1",
stat_compensation:
"sensor.energy_production_tarif_1_compensation",
entity_energy_price: null,
number_energy_price: null,
},
{
stat_energy_to: "sensor.energy_production_tarif_2",
stat_compensation:
"sensor.energy_production_tarif_2_compensation",
entity_energy_price: null,
number_energy_price: null,
},
],
cost_adjustment_day: 0,
},
{
type: "solar",
stat_energy_from: "sensor.solar_production",
stat_rate: "sensor.power_solar",
config_entry_solar_forecast: ["solar_forecast"],
},
{
/* {
type: "battery",
stat_energy_from: "sensor.battery_output",
stat_energy_to: "sensor.battery_input",
stat_rate: "sensor.power_battery",
},
}, */
{
type: "gas",
stat_energy_from: "sensor.energy_gas",
@@ -57,46 +63,25 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
entity_energy_price: null,
number_energy_price: null,
},
{
type: "water",
stat_energy_from: "sensor.energy_water",
stat_cost: "sensor.energy_water_cost",
entity_energy_price: null,
number_energy_price: null,
},
],
device_consumption: [
{
stat_consumption: "sensor.energy_car",
stat_rate: "sensor.power_car",
},
{
stat_consumption: "sensor.energy_ac",
stat_rate: "sensor.power_ac",
},
{
stat_consumption: "sensor.energy_washing_machine",
stat_rate: "sensor.power_washing_machine",
},
{
stat_consumption: "sensor.energy_dryer",
stat_rate: "sensor.power_dryer",
},
{
stat_consumption: "sensor.energy_heat_pump",
stat_rate: "sensor.power_heat_pump",
},
{
stat_consumption: "sensor.energy_boiler",
stat_rate: "sensor.power_boiler",
},
],
device_consumption_water: [
{
stat_consumption: "sensor.water_kitchen",
},
{
stat_consumption: "sensor.water_garden",
},
],
})

View File

@@ -1,5 +1,7 @@
import { convertEntities } from "../../../src/fake_data/entity";
export const mapEntities = () =>
Object.values({
convertEntities({
"zone.home": {
entity_id: "zone.home",
state: "zoning",
@@ -49,7 +51,7 @@ export const mapEntities = () =>
});
export const energyEntities = () =>
Object.values({
convertEntities({
"sensor.grid_fossil_fuel_percentage": {
entity_id: "sensor.grid_fossil_fuel_percentage",
state: "88.6",
@@ -152,38 +154,6 @@ export const energyEntities = () =>
unit_of_measurement: "EUR",
},
},
"sensor.power_grid": {
entity_id: "sensor.power_grid",
state: "500",
attributes: {
state_class: "measurement",
unit_of_measurement: "W",
},
},
"sensor.power_grid_return": {
entity_id: "sensor.power_grid_return",
state: "-100",
attributes: {
state_class: "measurement",
unit_of_measurement: "W",
},
},
"sensor.power_solar": {
entity_id: "sensor.power_solar",
state: "200",
attributes: {
state_class: "measurement",
unit_of_measurement: "W",
},
},
"sensor.power_battery": {
entity_id: "sensor.power_battery",
state: "100",
attributes: {
state_class: "measurement",
unit_of_measurement: "W",
},
},
"sensor.energy_gas_cost": {
entity_id: "sensor.energy_gas_cost",
state: "2",
@@ -201,15 +171,6 @@ export const energyEntities = () =>
unit_of_measurement: "m³",
},
},
"sensor.energy_water": {
entity_id: "sensor.energy_water",
state: "4000",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Water",
unit_of_measurement: "L",
},
},
"sensor.energy_car": {
entity_id: "sensor.energy_car",
state: "4",
@@ -264,58 +225,4 @@ export const energyEntities = () =>
unit_of_measurement: "kWh",
},
},
"sensor.power_car": {
entity_id: "sensor.power_car",
state: "40",
attributes: {
state_class: "measurement",
friendly_name: "Electric car",
unit_of_measurement: "W",
},
},
"sensor.power_ac": {
entity_id: "sensor.power_ac",
state: "30",
attributes: {
state_class: "measurement",
friendly_name: "Air conditioning",
unit_of_measurement: "W",
},
},
"sensor.power_washing_machine": {
entity_id: "sensor.power_washing_machine",
state: "60",
attributes: {
state_class: "measurement",
friendly_name: "Washing machine",
unit_of_measurement: "W",
},
},
"sensor.power_dryer": {
entity_id: "sensor.power_dryer",
state: "55",
attributes: {
state_class: "measurement",
friendly_name: "Dryer",
unit_of_measurement: "W",
},
},
"sensor.power_heat_pump": {
entity_id: "sensor.power_heat_pump",
state: "60",
attributes: {
state_class: "measurement",
friendly_name: "Heat pump",
unit_of_measurement: "W",
},
},
"sensor.power_boiler": {
entity_id: "sensor.power_boiler",
state: "70",
attributes: {
state_class: "measurement",
friendly_name: "Boiler",
unit_of_measurement: "W",
},
},
});

View File

@@ -1,4 +1,4 @@
import type { EntityRegistryEntry } from "../../../src/data/entity/entity_registry";
import type { EntityRegistryEntry } from "../../../src/data/entity_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockEntityRegistry = (

View File

@@ -1,12 +1,14 @@
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
let sidebarChangeCallback;
let changeFunction;
export const mockFrontend = (hass: MockHomeAssistant) => {
hass.mockWS("frontend/get_user_data", () => ({ value: null }));
hass.mockWS("frontend/get_user_data", () => ({
value: null,
}));
hass.mockWS("frontend/set_user_data", ({ key, value }) => {
if (key === "sidebar") {
sidebarChangeCallback?.({
changeFunction?.({
value: {
panelOrder: value.panelOrder || [],
hiddenPanels: value.hiddenPanels || [],
@@ -14,34 +16,15 @@ export const mockFrontend = (hass: MockHomeAssistant) => {
});
}
});
hass.mockWS("frontend/subscribe_user_data", (msg, _hass, onChange) => {
if (msg.key === "sidebar") {
sidebarChangeCallback = onChange;
}
onChange?.({ value: null });
// eslint-disable-next-line @typescript-eslint/no-empty-function
return () => {};
});
hass.mockWS(
"frontend/subscribe_system_data",
(_msg, currentHass, onChange) => {
onChange?.({
value: currentHass.systemData,
});
// eslint-disable-next-line @typescript-eslint/no-empty-function
return () => {};
}
);
hass.mockWS("labs/subscribe", (_msg, _currentHass, onChange) => {
hass.mockWS("frontend/subscribe_user_data", (_msg, _hass, onChange) => {
changeFunction = onChange;
onChange?.({
preview_feature: _msg.preview_feature,
domain: _msg.domain,
enabled: false,
is_built_in: true,
value: {
panelOrder: [],
hiddenPanels: [],
},
});
// eslint-disable-next-line @typescript-eslint/no-empty-function
return () => {};
});
hass.mockWS("repairs/list_issues", () => ({ issues: [] }));
hass.mockWS("frontend/get_themes", (_msg, currentHass) => currentHass.themes);
};

View File

@@ -1,4 +1,4 @@
import type { LabelRegistryEntry } from "../../../src/data/label/label_registry";
import type { LabelRegistryEntry } from "../../../src/data/label_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockLabelRegistry = (

View File

@@ -29,7 +29,6 @@ export const mockLovelace = (
hass.mockWS("lovelace/config/save", () => Promise.resolve());
hass.mockWS("lovelace/resources", () => Promise.resolve([]));
hass.mockWS("lovelace/dashboards/list", () => Promise.resolve([]));
};
customElements.whenDefined("hui-root").then(() => {

View File

@@ -17,15 +17,17 @@ const generateMeanStatistics = (
end: Date,
// eslint-disable-next-line default-param-last
period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number,
maxDiff: number
): StatisticValue[] => {
const statistics: StatisticValue[] = [];
let currentDate = new Date(start);
currentDate.setMinutes(0, 0, 0);
let lastVal = initValue;
const now = new Date();
while (end > currentDate && currentDate < now) {
const delta = Math.random() * maxDiff;
const mean = delta;
const mean = lastVal + delta;
statistics.push({
start: currentDate.getTime(),
end: currentDate.getTime(),
@@ -36,6 +38,7 @@ const generateMeanStatistics = (
state: mean,
sum: null,
});
lastVal = mean;
currentDate =
period === "day"
? addDays(currentDate, 1)
@@ -333,6 +336,7 @@ export const mockRecorder = (mockHass: MockHomeAssistant) => {
start,
end,
period,
state,
state * (state > 80 ? 0.05 : 0.1)
);
}

View File

@@ -7,18 +7,8 @@ export const mockTemplate = (hass: MockHomeAssistant) => {
})
);
hass.mockWS("render_template", (msg, _hass, onChange) => {
let result = msg.template;
// Simple variable substitution for demo purposes
if (msg.variables) {
for (const [key, value] of Object.entries(msg.variables)) {
result = result.replace(
new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, "g"),
String(value)
);
}
}
onChange!({
result,
result: msg.template,
listeners: { all: false, domains: [], entities: [], time: false },
});
// eslint-disable-next-line @typescript-eslint/no-empty-function

View File

@@ -1,5 +1,6 @@
// @ts-check
/* eslint-disable import/no-extraneous-dependencies */
import unusedImports from "eslint-plugin-unused-imports";
import globals from "globals";
import path from "node:path";
@@ -11,8 +12,6 @@ import eslintConfigPrettier from "eslint-config-prettier";
import { configs as litConfigs } from "eslint-plugin-lit";
import { configs as wcConfigs } from "eslint-plugin-wc";
import { configs as a11yConfigs } from "eslint-plugin-lit-a11y";
import html from "@html-eslint/eslint-plugin";
import importX from "eslint-plugin-import-x";
const _filename = fileURLToPath(import.meta.url);
const _dirname = path.dirname(_filename);
@@ -22,27 +21,8 @@ const compat = new FlatCompat({
allConfig: js.configs.all,
});
// Load airbnb-base via FlatCompat for non-import rules only.
// eslint-plugin-import is incompatible with ESLint 10 (uses removed APIs),
// so we strip its plugin/rules/settings and use eslint-plugin-import-x instead.
const airbnbConfigs = compat.extends("airbnb-base").map((config) => {
const { plugins = {}, rules = {}, settings = {}, ...rest } = config;
return {
...rest,
plugins: Object.fromEntries(
Object.entries(plugins).filter(([key]) => key !== "import")
),
rules: Object.fromEntries(
Object.entries(rules).filter(([key]) => !key.startsWith("import/"))
),
settings: Object.fromEntries(
Object.entries(settings).filter(([key]) => !key.startsWith("import/"))
),
};
});
export default tseslint.config(
...airbnbConfigs,
...compat.extends("airbnb-base"),
eslintConfigPrettier,
litConfigs["flat/all"],
tseslint.configs.recommended,
@@ -50,7 +30,6 @@ export default tseslint.config(
tseslint.configs.stylistic,
wcConfigs["flat/recommended"],
a11yConfigs.recommended,
importX.flatConfigs.recommended,
{
plugins: {
"unused-imports": unusedImports,
@@ -64,6 +43,7 @@ export default tseslint.config(
__BUILD__: false,
__VERSION__: false,
__STATIC_PATH__: false,
__SUPERVISOR__: false,
},
parser: tseslint.parser,
@@ -78,7 +58,7 @@ export default tseslint.config(
},
settings: {
"import-x/resolver": {
"import/resolver": {
webpack: {
config: "./rspack.config.cjs",
},
@@ -107,20 +87,12 @@ export default tseslint.config(
"prefer-destructuring": "off",
"no-restricted-globals": [2, "event"],
"prefer-promise-reject-errors": "off",
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
"object-curly-newline": "off",
"default-case": "off",
"wc/no-self-class": "off",
"no-shadow": "off",
"no-use-before-define": "off",
"import/prefer-default-export": "off",
"import/no-default-export": "off",
"import/no-unresolved": "off",
"import/no-cycle": "off",
// import-x rules (migrated from eslint-plugin-import / airbnb-base)
"import-x/named": "off",
"import-x/prefer-default-export": "off",
"import-x/no-default-export": "off",
"import-x/no-unresolved": "off",
"import-x/no-cycle": "off",
"import-x/extensions": [
"import/extensions": [
"error",
"ignorePackages",
{
@@ -128,24 +100,12 @@ export default tseslint.config(
js: "never",
},
],
"import-x/no-mutable-exports": "error",
"import-x/no-amd": "error",
"import-x/first": "error",
"import-x/order": [
"error",
{ groups: [["builtin", "external", "internal"]] },
],
"import-x/newline-after-import": "error",
"import-x/no-absolute-path": "error",
"import-x/no-dynamic-require": "error",
"import-x/no-webpack-loader-syntax": "error",
"import-x/no-named-default": "error",
"import-x/no-self-import": "error",
"import-x/no-useless-path-segments": ["error", { commonjs: true }],
"import-x/no-import-module-exports": ["error", { exceptions: [] }],
"import-x/no-relative-packages": "error",
// TypeScript rules
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
"object-curly-newline": "off",
"default-case": "off",
"wc/no-self-class": "off",
"no-shadow": "off",
"@typescript-eslint/camelcase": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-use-before-define": "off",
@@ -225,26 +185,7 @@ export default tseslint.config(
allowObjectTypes: "always",
},
],
},
},
{
files: ["src/util/recorder-worklet.js"],
languageOptions: {
globals: globals.audioWorklet,
},
},
{
files: ["src/entrypoints/service-worker.ts"],
languageOptions: {
globals: globals.serviceworker,
},
},
{
plugins: {
html,
},
rules: {
"html/no-invalid-attr-value": "error",
"no-use-before-define": "off",
},
}
);

View File

@@ -1,11 +1,11 @@
import type { TemplateResult, PropertyValues } from "lit";
import "@material/mwc-button/mwc-button";
import type { Button } from "@material/mwc-button";
import type { TemplateResult } from "lit";
import { html, LitElement, css, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-card";
import "../../../src/components/ha-button";
import type { HaButton } from "../../../src/components/ha-button";
@customElement("demo-black-white-row")
class DemoBlackWhiteRow extends LitElement {
@@ -25,9 +25,12 @@ class DemoBlackWhiteRow extends LitElement {
<slot name="light"></slot>
</div>
<div class="card-actions">
<ha-button .disabled=${this.disabled} @click=${this.handleSubmit}>
<mwc-button
.disabled=${this.disabled}
@click=${this.handleSubmit}
>
Submit
</ha-button>
</mwc-button>
</div>
</ha-card>
</div>
@@ -37,9 +40,12 @@ class DemoBlackWhiteRow extends LitElement {
<slot name="dark"></slot>
</div>
<div class="card-actions">
<ha-button .disabled=${this.disabled} @click=${this.handleSubmit}>
<mwc-button
.disabled=${this.disabled}
@click=${this.handleSubmit}
>
Submit
</ha-button>
</mwc-button>
</div>
</ha-card>
${this.value
@@ -50,7 +56,7 @@ class DemoBlackWhiteRow extends LitElement {
`;
}
firstUpdated(changedProps: PropertyValues<this>) {
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
@@ -68,7 +74,7 @@ class DemoBlackWhiteRow extends LitElement {
}
handleSubmit(ev) {
const content = (ev.target as HaButton).closest(".content")!;
const content = (ev.target as Button).closest(".content")!;
fireEvent(this, "submitted" as any, {
slot: content.classList.contains("light") ? "light" : "dark",
});

View File

@@ -1,5 +1,5 @@
import { load } from "js-yaml";
import type { PropertyValues } from "lit";
import type { PropertyValueMap } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -60,7 +60,9 @@ class DemoCard extends LitElement {
this._size = await this._card?.getCardSize();
}
protected update(_changedProperties: PropertyValues<this>): void {
protected update(
_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
): void {
super.update(_changedProperties);
this._updateSize();
}

View File

@@ -1,11 +1,10 @@
import { LitElement, css, html, nothing } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../src/components/ha-card";
import "../../../src/dialogs/more-info/more-info-content";
import "../../../src/state-summary/state-card-content";
import "../ha-demo-options";
import type { HomeAssistant } from "../../../src/types";
import { computeShowNewMoreInfo } from "../../../src/dialogs/more-info/const";
@customElement("demo-more-info")
class DemoMoreInfo extends LitElement {
@@ -22,13 +21,11 @@ class DemoMoreInfo extends LitElement {
<div class="root">
<div id="card">
<ha-card>
${!computeShowNewMoreInfo(state)
? html`<state-card-content
.stateObj=${state}
.hass=${this.hass}
in-dialog
></state-card-content>`
: nothing}
<state-card-content
.stateObj=${state}
.hass=${this.hass}
in-dialog
></state-card-content>
<more-info-content
.hass=${this.hass}

View File

@@ -1106,7 +1106,7 @@ export default {
friendly_name: "Philips Hue",
entity_picture: null,
description:
"Press the button on the bridge to register Philips Hue with Home Assistant.",
"Press the button on the bridge to register Philips Hue with Home Assistant.\n\n![Description image](/static/images/config_philips_hue.jpg)",
submit_caption: "I have pressed the button",
},
last_changed: "2018-07-19T10:44:46.515160+00:00",

View File

@@ -1,253 +1,165 @@
import { getEntity } from "../../../src/fake_data/entity";
export const createMediaPlayerEntities = () => [
{
entity_id: "media_player.music_paused",
state: "paused",
attributes: {
friendly_name: "Pausing The Music",
media_content_type: "music",
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
media_artist: "Technohead",
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
// Select Source + Stop + Clear + Play + Shuffle Set
supported_features: 64063,
entity_picture: "/images/album_cover_2.jpg",
media_duration: 300,
media_position: 50,
media_position_updated_at: new Date(
// 23 seconds in
new Date().getTime() - 23000
).toISOString(),
volume_level: 0.5,
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
source: "AirPlay",
sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"],
sound_mode: "Music",
},
},
{
entity_id: "media_player.music_playing",
state: "playing",
attributes: {
friendly_name: "Playing The Music",
media_content_type: "music",
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
media_artist: "Technohead",
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
// Select Source + Stop + Clear + Play + Shuffle Set + Browse Media + Grouping
supported_features: 784959,
entity_picture: "/images/album_cover.jpg",
media_duration: 300,
media_position: 0,
media_position_updated_at: new Date(
// 23 seconds in
new Date().getTime() - 23000
).toISOString(),
volume_level: 0.5,
sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"],
sound_mode: "Music",
group_members: ["media_player.playing", "media_player.stream_playing"],
},
},
{
entity_id: "media_player.stream_playing",
state: "playing",
attributes: {
friendly_name: "Playing the Stream",
media_content_type: "movie",
media_title: "Epic sax guy 10 hours",
app_name: "YouTube",
entity_picture: "/images/frenck.jpg",
// Pause + Next Track + Play + Browse Media
supported_features: 147489,
},
},
{
entity_id: "media_player.stream_paused",
state: "paused",
attributes: {
friendly_name: "Paused the Stream",
media_content_type: "movie",
media_title: "Epic sax guy 10 hours",
app_name: "YouTube",
entity_picture: "/images/frenck.jpg",
// Pause + Next Track + Play
supported_features: 16417,
},
},
{
entity_id: "media_player.stream_playing_previous",
state: "playing",
attributes: {
friendly_name: 'Playing the Stream (with "previous" support)',
media_content_type: "movie",
media_title: "Epic sax guy 10 hours",
app_name: "YouTube",
entity_picture: "/images/frenck.jpg",
// Pause + Previous Track + Play
supported_features: 16401,
},
},
{
entity_id: "media_player.tv_playing",
state: "playing",
attributes: {
friendly_name: "Playing non-skip TV Show",
media_content_type: "tvshow",
media_title: "Chapter 1",
media_series_title: "House of Cards",
app_name: "Netflix",
entity_picture: "/images/netflix.jpg",
// Pause
supported_features: 1,
},
},
{
entity_id: "media_player.sonos_idle",
state: "idle",
attributes: {
friendly_name: "Sonos Idle",
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
// Select Source + Stop + Clear + Play + Shuffle Set
supported_features: 64063,
volume_level: 0.33,
is_volume_muted: true,
},
},
{
entity_id: "media_player.idle_browse_media",
state: "idle",
attributes: {
friendly_name: "Idle waiting for Browse Media (e.g. Spotify)",
// Pause + Seek + Volume Set + Previous Track + Next Track + Play Media +
// Select Source + Play + Shuffle Set + Browse Media
supported_features: 182839,
volume_level: 0.79,
},
},
{
entity_id: "media_player.theater_off",
state: "off",
attributes: {
friendly_name: "TV Off",
// On + Off + Play + Next + Pause
supported_features: 16801,
},
},
{
entity_id: "media_player.theater_on",
state: "on",
attributes: {
friendly_name: "TV On",
// On + Off + Play + Next + Pause
supported_features: 16801,
},
},
{
entity_id: "media_player.theater_off_static",
state: "off",
attributes: {
friendly_name: "TV Off (cannot be switched on)",
// Off + Next + Pause
supported_features: 289,
},
},
{
entity_id: "media_player.theater_on_static",
state: "on",
attributes: {
friendly_name: "TV On (cannot be switched off)",
// On + Next + Pause
supported_features: 161,
},
},
{
entity_id: "media_player.android_cast",
state: "playing",
attributes: {
friendly_name: "Casting App (no supported features)",
media_title: "Android Screen Casting",
app_name: "Screen Mirroring",
},
},
{
entity_id: "media_player.image_display",
state: "playing",
attributes: {
friendly_name: "Digital Picture Frame",
media_content_type: "image",
media_title: "Famous Painting",
media_artist: "Famous Artist",
entity_picture: "/images/sunflowers.jpg",
// On + Off + Browse Media
supported_features: 131456,
},
},
{
entity_id: "media_player.unavailable",
state: "unavailable",
attributes: {
friendly_name: "Player Unavailable",
// Pause + Volume Set + Volume Mute + Previous Track + Next Track +
// Play Media + Stop + Play
supported_features: 21437,
},
},
{
entity_id: "media_player.unknown",
state: "unknown",
attributes: {
friendly_name: "Player Unknown",
// Pause + Volume Set + Volume Mute + Previous Track + Next Track +
// Play Media + Stop + Play
supported_features: 21437,
},
},
{
entity_id: "media_player.playing",
state: "playing",
attributes: {
friendly_name: "Player Playing (no Pause support)",
// Volume Set + Volume Mute + Previous Track + Next Track +
// Play Media + Stop + Play
supported_features: 21436,
volume_level: 1,
},
},
{
entity_id: "media_player.idle",
state: "idle",
attributes: {
friendly_name: "Player Idle",
// Pause + Volume Set + Volume Mute + Previous Track + Next Track +
// Play Media + Stop + Play
supported_features: 21437,
volume_level: 0,
},
},
{
entity_id: "media_player.receiver_on",
state: "on",
attributes: {
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"],
volume_level: 0.63,
is_volume_muted: false,
source: "TV",
sound_mode: "Movie",
friendly_name: "Receiver (selectable sources)",
// Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode
supported_features: 84364,
},
},
{
entity_id: "media_player.receiver_off",
state: "off",
attributes: {
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"],
friendly_name: "Receiver (selectable sources)",
// Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode
supported_features: 84364,
},
},
getEntity("media_player", "music_paused", "paused", {
friendly_name: "Pausing The Music",
media_content_type: "music",
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
media_artist: "Technohead",
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
// Select Source + Stop + Clear + Play + Shuffle Set
supported_features: 64063,
entity_picture: "/images/album_cover_2.jpg",
media_duration: 300,
media_position: 50,
media_position_updated_at: new Date(
// 23 seconds in
new Date().getTime() - 23000
).toISOString(),
volume_level: 0.5,
}),
getEntity("media_player", "music_playing", "playing", {
friendly_name: "Playing The Music",
media_content_type: "music",
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
media_artist: "Technohead",
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
// Select Source + Stop + Clear + Play + Shuffle Set + Browse Media
supported_features: 195135,
entity_picture: "/images/album_cover.jpg",
media_duration: 300,
media_position: 0,
media_position_updated_at: new Date(
// 23 seconds in
new Date().getTime() - 23000
).toISOString(),
volume_level: 0.5,
}),
getEntity("media_player", "stream_playing", "playing", {
friendly_name: "Playing the Stream",
media_content_type: "movie",
media_title: "Epic sax guy 10 hours",
app_name: "YouTube",
entity_picture: "/images/frenck.jpg",
// Pause + Next Track + Play + Browse Media
supported_features: 147489,
}),
getEntity("media_player", "stream_paused", "paused", {
friendly_name: "Paused the Stream",
media_content_type: "movie",
media_title: "Epic sax guy 10 hours",
app_name: "YouTube",
entity_picture: "/images/frenck.jpg",
// Pause + Next Track + Play
supported_features: 16417,
}),
getEntity("media_player", "stream_playing_previous", "playing", {
friendly_name: 'Playing the Stream (with "previous" support)',
media_content_type: "movie",
media_title: "Epic sax guy 10 hours",
app_name: "YouTube",
entity_picture: "/images/frenck.jpg",
// Pause + Previous Track + Play
supported_features: 16401,
}),
getEntity("media_player", "tv_playing", "playing", {
friendly_name: "Playing non-skip TV Show",
media_content_type: "tvshow",
media_title: "Chapter 1",
media_series_title: "House of Cards",
app_name: "Netflix",
entity_picture: "/images/netflix.jpg",
// Pause
supported_features: 1,
}),
getEntity("media_player", "sonos_idle", "idle", {
friendly_name: "Sonos Idle",
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
// Select Source + Stop + Clear + Play + Shuffle Set
supported_features: 64063,
volume_level: 0.33,
is_volume_muted: true,
}),
getEntity("media_player", "idle_browse_media", "idle", {
friendly_name: "Idle waiting for Browse Media (e.g. Spotify)",
// Pause + Seek + Volume Set + Previous Track + Next Track + Play Media +
// Select Source + Play + Shuffle Set + Browse Media
supported_features: 182839,
volume_level: 0.79,
}),
getEntity("media_player", "theater_off", "off", {
friendly_name: "TV Off",
// On + Off + Play + Next + Pause
supported_features: 16801,
}),
getEntity("media_player", "theater_on", "on", {
friendly_name: "TV On",
// On + Off + Play + Next + Pause
supported_features: 16801,
}),
getEntity("media_player", "theater_off_static", "off", {
friendly_name: "TV Off (cannot be switched on)",
// Off + Next + Pause
supported_features: 289,
}),
getEntity("media_player", "theater_on_static", "on", {
friendly_name: "TV On (cannot be switched off)",
// On + Next + Pause
supported_features: 161,
}),
getEntity("media_player", "android_cast", "playing", {
friendly_name: "Casting App (no supported features)",
media_title: "Android Screen Casting",
app_name: "Screen Mirroring",
}),
getEntity("media_player", "image_display", "playing", {
friendly_name: "Digital Picture Frame",
media_content_type: "image",
media_title: "Famous Painting",
media_artist: "Famous Artist",
entity_picture: "/images/sunflowers.jpg",
// On + Off + Browse Media
supported_features: 131456,
}),
getEntity("media_player", "unavailable", "unavailable", {
friendly_name: "Player Unavailable",
// Pause + Volume Set + Volume Mute + Previous Track + Next Track +
// Play Media + Stop + Play
supported_features: 21437,
}),
getEntity("media_player", "unknown", "unknown", {
friendly_name: "Player Unknown",
// Pause + Volume Set + Volume Mute + Previous Track + Next Track +
// Play Media + Stop + Play
supported_features: 21437,
}),
getEntity("media_player", "playing", "playing", {
friendly_name: "Player Playing (no Pause support)",
// Volume Set + Volume Mute + Previous Track + Next Track +
// Play Media + Stop + Play
supported_features: 21436,
volume_level: 1,
}),
getEntity("media_player", "idle", "idle", {
friendly_name: "Player Idle",
// Pause + Volume Set + Volume Mute + Previous Track + Next Track +
// Play Media + Stop + Play
supported_features: 21437,
volume_level: 0,
}),
getEntity("media_player", "receiver_on", "on", {
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
volume_level: 0.63,
is_volume_muted: false,
source: "TV",
friendly_name: "Receiver (selectable sources)",
// Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode
supported_features: 84364,
}),
getEntity("media_player", "receiver_off", "off", {
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
friendly_name: "Receiver (selectable sources)",
// Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode
supported_features: 84364,
}),
];

View File

@@ -1,82 +1,72 @@
import { getEntity } from "../../../src/fake_data/entity";
export const createPlantEntities = () => [
{
entity_id: "plant.lemon_tree",
state: "ok",
attributes: {
problem: "none",
sensors: {
moisture: "sensor.lemon_tree_moisture",
battery: "sensor.lemon_tree_battery",
temperature: "sensor.lemon_tree_temperature",
conductivity: "sensor.lemon_tree_conductivity",
brightness: "sensor.lemon_tree_brightness",
},
unit_of_measurement_dict: {
temperature: "°C",
moisture: "%",
brightness: "lx",
battery: "%",
conductivity: "μS/cm",
},
moisture: 54,
battery: 95,
temperature: 15.6,
conductivity: 1,
brightness: 12,
max_brightness: 20,
friendly_name: "Lemon Tree",
getEntity("plant", "lemon_tree", "ok", {
problem: "none",
sensors: {
moisture: "sensor.lemon_tree_moisture",
battery: "sensor.lemon_tree_battery",
temperature: "sensor.lemon_tree_temperature",
conductivity: "sensor.lemon_tree_conductivity",
brightness: "sensor.lemon_tree_brightness",
},
},
{
entity_id: "plant.apple_tree",
state: "ok",
attributes: {
problem: "brightness",
sensors: {
moisture: "sensor.apple_tree_moisture",
battery: "sensor.apple_tree_battery",
temperature: "sensor.apple_tree_temperature",
conductivity: "sensor.apple_tree_conductivity",
brightness: "sensor.apple_tree_brightness",
},
unit_of_measurement_dict: {
temperature: "°C",
moisture: "%",
brightness: "lx",
battery: "%",
conductivity: "μS/cm",
},
moisture: 54,
battery: 2,
temperature: 15.6,
conductivity: 1,
brightness: 25,
max_brightness: 20,
friendly_name: "Apple Tree",
unit_of_measurement_dict: {
temperature: "°C",
moisture: "%",
brightness: "lx",
battery: "%",
conductivity: "μS/cm",
},
},
{
entity_id: "plant.sunflowers",
state: "ok",
attributes: {
problem: "moisture, temperature, conductivity",
sensors: {
moisture: "sensor.sunflowers_moisture",
temperature: "sensor.sunflowers_temperature",
conductivity: "sensor.sunflowers_conductivity",
brightness: "sensor.sunflowers_brightness",
},
unit_of_measurement_dict: {
temperature: "°C",
moisture: "%",
brightness: "lx",
conductivity: "μS/cm",
},
moisture: 54,
temperature: 15.6,
conductivity: 1,
brightness: 25,
entity_picture: "/images/sunflowers.jpg",
moisture: 54,
battery: 95,
temperature: 15.6,
conductivity: 1,
brightness: 12,
max_brightness: 20,
friendly_name: "Lemon Tree",
}),
getEntity("plant", "apple_tree", "ok", {
problem: "brightness",
sensors: {
moisture: "sensor.apple_tree_moisture",
battery: "sensor.apple_tree_battery",
temperature: "sensor.apple_tree_temperature",
conductivity: "sensor.apple_tree_conductivity",
brightness: "sensor.apple_tree_brightness",
},
},
unit_of_measurement_dict: {
temperature: "°C",
moisture: "%",
brightness: "lx",
battery: "%",
conductivity: "μS/cm",
},
moisture: 54,
battery: 2,
temperature: 15.6,
conductivity: 1,
brightness: 25,
max_brightness: 20,
friendly_name: "Apple Tree",
}),
getEntity("plant", "sunflowers", "ok", {
problem: "moisture, temperature, conductivity",
sensors: {
moisture: "sensor.sunflowers_moisture",
temperature: "sensor.sunflowers_temperature",
conductivity: "sensor.sunflowers_conductivity",
brightness: "sensor.sunflowers_brightness",
},
unit_of_measurement_dict: {
temperature: "°C",
moisture: "%",
brightness: "lx",
conductivity: "μS/cm",
},
moisture: 54,
temperature: 15.6,
conductivity: 1,
brightness: 25,
entity_picture: "/images/sunflowers.jpg",
}),
];

View File

@@ -135,7 +135,7 @@ class HaGallery extends LitElement {
`;
}
firstUpdated(changedProps: PropertyValues<this>) {
firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.addEventListener("show-notification", (ev) =>
@@ -208,7 +208,7 @@ class HaGallery extends LitElement {
}
.sidebar a[active]::before {
border-radius: var(--ha-border-radius-lg);
border-radius: 12px;
position: absolute;
top: 0;
right: 2px;
@@ -241,7 +241,7 @@ class HaGallery extends LitElement {
text-align: center;
margin: 16px;
padding: 16px;
border-radius: var(--ha-border-radius-lg);
border-radius: 12px;
background-color: var(--primary-background-color);
}

View File

@@ -1,29 +1,21 @@
import { dump } from "js-yaml";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-yaml-editor";
import type { Action } from "../../../../src/data/script";
import { describeAction } from "../../../../src/data/script_i18n";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types";
const ENTITIES = [
{
entity_id: "scene.kitchen_morning",
state: "scening",
attributes: {
friendly_name: "Kitchen Morning",
},
},
{
entity_id: "media_player.kitchen",
state: "playing",
attributes: {
friendly_name: "Sonos Kitchen",
},
},
getEntity("scene", "kitchen_morning", "scening", {
friendly_name: "Kitchen Morning",
}),
getEntity("media_player", "kitchen", "playing", {
friendly_name: "Sonos Kitchen",
}),
];
const ACTIONS = [
@@ -150,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
<div class="action">
<span>
${this._action
? describeAction(this.hass, [], this._action)
? describeAction(this.hass, [], [], {}, this._action)
: "<invalid YAML>"}
</span>
<ha-yaml-editor
@@ -163,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement {
${ACTIONS.map(
(conf) => html`
<div class="action">
<span>${describeAction(this.hass, [], conf as any)}</span>
<span>${describeAction(this.hass, [], [], {}, conf as any)}</span>
<pre>${dump(conf)}</pre>
</div>
`
@@ -172,7 +164,7 @@ export class DemoAutomationDescribeAction extends LitElement {
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");

View File

@@ -1,36 +1,24 @@
import { dump } from "js-yaml";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-yaml-editor";
import type { Condition } from "../../../../src/data/automation";
import { describeCondition } from "../../../../src/data/automation_i18n";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types";
const ENTITIES = [
{
entity_id: "light.kitchen",
state: "on",
attributes: {
friendly_name: "Kitchen Light",
},
},
{
entity_id: "device_tracker.person",
state: "home",
attributes: {
friendly_name: "Person",
},
},
{
entity_id: "zone.home",
state: "",
attributes: {
friendly_name: "Home",
},
},
getEntity("light", "kitchen", "on", {
friendly_name: "Kitchen Light",
}),
getEntity("device_tracker", "person", "home", {
friendly_name: "Person",
}),
getEntity("zone", "home", "", {
friendly_name: "Home",
}),
];
const conditions: Condition[] = [
@@ -97,7 +85,7 @@ export class DemoAutomationDescribeCondition extends LitElement {
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");

View File

@@ -1,36 +1,24 @@
import { dump } from "js-yaml";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-yaml-editor";
import type { LegacyTrigger } from "../../../../src/data/automation";
import type { Trigger } from "../../../../src/data/automation";
import { describeTrigger } from "../../../../src/data/automation_i18n";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types";
const ENTITIES = [
{
entity_id: "light.kitchen",
state: "on",
attributes: {
friendly_name: "Kitchen Light",
},
},
{
entity_id: "person.person",
state: "",
attributes: {
friendly_name: "Person",
},
},
{
entity_id: "zone.home",
state: "",
attributes: {
friendly_name: "Home",
},
},
getEntity("light", "kitchen", "on", {
friendly_name: "Kitchen Light",
}),
getEntity("person", "person", "", {
friendly_name: "Person",
}),
getEntity("zone", "home", "", {
friendly_name: "Home",
}),
];
const triggers = [
@@ -78,7 +66,7 @@ const triggers = [
},
];
const initialTrigger: LegacyTrigger = {
const initialTrigger: Trigger = {
trigger: "state",
entity_id: "light.kitchen",
};
@@ -120,7 +108,7 @@ export class DemoAutomationDescribeTrigger extends LitElement {
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");

View File

@@ -18,6 +18,7 @@ import { HaDeviceAction } from "../../../../src/panels/config/automation/action/
import { HaEventAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-event";
import { HaIfAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-if";
import { HaParallelAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-parallel";
import { HaPlayMediaAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-play_media";
import { HaRepeatAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-repeat";
import { HaSequenceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-sequence";
import { HaServiceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-service";
@@ -31,6 +32,7 @@ const SCHEMAS: { name: string; actions: Action[] }[] = [
{ name: "Service", actions: [HaServiceAction.defaultConfig] },
{ name: "Condition", actions: [HaConditionAction.defaultConfig] },
{ name: "Delay", actions: [HaDelayAction.defaultConfig] },
{ name: "Play media", actions: [HaPlayMediaAction.defaultConfig] },
{ name: "Wait", actions: [HaWaitAction.defaultConfig] },
{ name: "WaitForTrigger", actions: [HaWaitForTriggerAction.defaultConfig] },
{ name: "Repeat", actions: [HaRepeatAction.defaultConfig] },

View File

@@ -18,6 +18,7 @@ import { HaEventTrigger } from "../../../../src/panels/config/automation/trigger
import { HaGeolocationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location";
import { HaHassTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-homeassistant";
import { HaTriggerList } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-list";
import { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
import { HaNumericStateTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state";
import { HaPersistentNotificationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-persistent_notification";
import { HaStateTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-state";
@@ -37,6 +38,11 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
triggers: [{ ...HaStateTrigger.defaultConfig }],
},
{
name: "MQTT",
triggers: [{ ...HaMQTTTrigger.defaultConfig }],
},
{
name: "GeoLocation",
triggers: [{ ...HaGeolocationTrigger.defaultConfig }],

View File

@@ -1,6 +1,5 @@
/* eslint-disable lit/no-template-arrow */
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../src/components/ha-card";
@@ -52,7 +51,7 @@ export class DemoAutomationTraceTimeline extends LitElement {
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");

View File

@@ -1,6 +1,5 @@
/* eslint-disable lit/no-template-arrow */
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-card";
@@ -52,7 +51,7 @@ export class DemoAutomationTrace extends LitElement {
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");

View File

@@ -10,9 +10,7 @@ As a community, we are proud of our logo. Follow these guidelines to ensure it a
![Logo](/images/brand/logo.png)
<ha-alert alert-type="info">
This logo is trademarked and the property of the Open Home Foundation. This means it is not available for commercial use without express written permission from the foundation. We regard commercial use as anything designed to market or promote a product, software or service that is for sale. Please contact <a href="mailto:partner@openhomefoundation.org">partner@openhomefoundation.org</a> for further information
</ha-alert>
Please note that this logo is not released under the CC license. All rights reserved.
# Design

View File

@@ -1 +0,0 @@
import "../../../../src/components/ha-alert";

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