Skip to content

Commit b67a7f5

Browse files
committed
Updated dependencies, improved codespace support and added auto discovery and installation of local plugins and integrations
1 parent 13f3ecb commit b67a7f5

File tree

18 files changed

+695
-120
lines changed

18 files changed

+695
-120
lines changed

.github/workflows/build-and-push-image.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,10 @@ jobs:
1919
context: .
2020
push: true
2121
tags: dcbr/hass-custom-devcontainer:latest
22+
- name: Docker Hub Description
23+
uses: peter-evans/dockerhub-description@v4
24+
with:
25+
username: ${{ secrets.DOCKERHUB_USERNAME }}
26+
password: ${{ secrets.DOCKERHUB_TOKEN }}
27+
repository: dcbr/hass-custom-devcontainer
28+
short-description: ${{ github.event.repository.description }}

.vscode/tasks.json

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,32 @@
1212
{
1313
"label": "Test",
1414
"type": "shell",
15-
"command": "./test.sh",
15+
"command": "./test.sh ${input:mode}",
1616
"problemMatcher": []
17+
},
18+
{
19+
"label": "Test Attach",
20+
"type": "shell",
21+
"command": "docker exec -it test sudo -E bash",
22+
"problemMatcher": []
23+
}
24+
],
25+
"inputs": [
26+
{
27+
"id": "mode",
28+
"type":"pickString",
29+
"description": "Mode:",
30+
"default": "",
31+
"options": [
32+
{
33+
"label": "Default",
34+
"value": ""
35+
},
36+
{
37+
"label": "Dev",
38+
"value": "dev"
39+
}
40+
]
1741
}
1842
]
1943
}

Dockerfile

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,55 @@
1-
#FROM homeassistant/home-assistant:dev
2-
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.10
1+
FROM mcr.microsoft.com/devcontainers/python:3.13
32

43
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
54

5+
# Initialize dev environment (copied from https://github.com/home-assistant/core/blob/dev/Dockerfile.dev)
66
RUN \
77
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
88
&& apt-get update \
99
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
1010
bluez \
11+
ffmpeg \
1112
libudev-dev \
1213
libavformat-dev \
1314
libavcodec-dev \
1415
libavdevice-dev \
1516
libavutil-dev \
17+
libgammu-dev \
1618
libswscale-dev \
1719
libswresample-dev \
1820
libavfilter-dev \
1921
libpcap-dev \
20-
git \
21-
libffi-dev \
22-
libssl-dev \
23-
libjpeg-dev \
24-
zlib1g-dev \
25-
autoconf \
26-
build-essential \
27-
libopenjp2-7 \
28-
libtiff5 \
2922
libturbojpeg0 \
30-
tzdata \
23+
libyaml-dev \
24+
libxml2 \
25+
git \
26+
cmake \
27+
inotify-tools \
3128
&& apt-get clean \
32-
&& rm -rf /var/lib/apt/lists/* \
33-
&& source /usr/local/share/nvm/nvm.sh \
34-
&& nvm install 14 \
35-
&& pip install --upgrade wheel pip
29+
&& rm -rf /var/lib/apt/lists/*
30+
31+
# Install Node
32+
RUN \
33+
source /usr/local/share/nvm/nvm.sh \
34+
&& nvm install 22
3635

3736
EXPOSE 8123
3837

39-
VOLUME /config
38+
VOLUME /workspace
39+
RUN mkdir -p /config/www/workspace /config/custom_components
4040

41-
COPY requirements.txt /tmp/requirements.txt
42-
RUN pip install -r /tmp/requirements.txt
43-
COPY src/container /usr/bin
44-
COPY src/hassfest /usr/bin
41+
COPY src/ /usr/bin/
42+
COPY scripts/ /usr/src/scripts/
4543

44+
# Setup python virtual environment
45+
RUN \
46+
pip3 install --upgrade wheel pip \
47+
&& pip3 install uv
4648
USER vscode
49+
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
50+
RUN uv venv $VIRTUAL_ENV
51+
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
52+
COPY requirements.txt /tmp/requirements.txt
53+
RUN uv pip install -r /tmp/requirements.txt
4754

48-
CMD sudo -E container
55+
CMD sudo -E container

README.md

Lines changed: 77 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,75 @@
55
```
66
docker run --rm -it \
77
-p 5000:8123 \
8-
-v $(pwd):/workspaces/test \
9-
-v $(pwd):/config/www/workspace \
10-
-e LOVELACE_LOCAL_FILES="myplugin.js"
11-
-e LOVELACE_PLUGINS="thomasloven/lovelace-card-mod thomasloven/lovelace-auto-entities custom-cards/button-card" \
8+
-v $(pwd)/test:/workspace/test \
9+
-v $(pwd)/dist:/workspace/dist \
10+
-v $(pwd)/custom_components:/workspace/custom_components \
11+
-e LOVELACE_PLUGINS="thomasloven/lovelace-card-mod;custom-cards/button-card" \
1212
dcbr/hass-custom-devcontainer
1313
```
1414

1515
The default action of the image is to run `container`, which will
16-
- Make sure there's a basic Home Assistant configuration in `/config`
17-
- Add a default admin user to Home Assistant
18-
- Skip the onboarding procedure
19-
- Download and install [HACS](https://hacs.xyz)
20-
- Download lovelace plugins from github
21-
- Add plugins to lovelace configuration
22-
- Start Home Assistant with `-v`
16+
- Make sure there's a basic Home Assistant configuration in `/config` based on the test configuration provided in the workspace (`/workspace/test/config`).
17+
- Add a default admin user to Home Assistant.
18+
- Skip the onboarding procedure.
19+
- Download and install [HACS](https://hacs.xyz) (optionally).
20+
- Download lovelace plugins from github (optionally).
21+
- Add downloaded and local plugins to lovelace configuration. The local plugins are fetched from the distribution folder in the workspace (`/workspace/dist`).
22+
- Add local integrations to the configuration. The local integrations are fetched from the custom components folder in the workspace (`/workspace/custom_components`).
23+
- Start Home Assistant with `-v`.
2324

25+
## Changelog
26+
27+
This is a fork from [thomasloven/hass-custom-devcontainer](https://github.com/thomasloven/hass-custom-devcontainer) with following changes:
28+
- Updated python environment and dependencies
29+
- Auto discovery and installation of locally developed plugins and integrations mounted in the `/workspace/dist` and `/workspace/custom_components` folders
30+
- Test configuration files mounted in the `/workspace/test/config` folder are preprocessed and copied to `/config` to be used by Home Assistant
31+
- Added support for bypassing the login form and for trusting the specified proxies
32+
- Built-in support for running this container as a codespace (see below)
2433

2534
### Environment Variables
2635

2736
| Name | Description | Default |
2837
|---|---|---|
2938
| `HASS_USERNAME` | The username of the default user | `dev` |
3039
| `HASS_PASSWORD` | The password of the default user | `dev` |
31-
| `LOVELACE_PLUGINS` | List of lovelac plugins to download from github | Empty |
32-
| `LOVELACE_LOCAL_FILES` | List of filenames in `/config/www/workspace` to add as lovelace resources | Emtpy |
40+
| `HASS_BYPASS_LOGIN` | Flag indicating whether to bypass the login form | `false` |
41+
| `HASS_TRUSTED_PROXIES` | Semicolon separated list of proxy ip addresses to trust | Empty (`::1` in codespace) |
42+
| `HASS_AUTO_RESTART` | Flag indicating whether Home Assistant should be automatically restarted once a change in the test configuration or local module files is detected | `true` |
43+
| `INSTALL_HACS` | Flag indicating whether to install HACS | `false` |
44+
| `LOVELACE_PLUGINS` | Semicolon separated list of lovelace plugins to download from github | Empty |
45+
| `ENV_FILE` | Path to environment file | Empty |
46+
| `CODESPACE_PLUGINS` | Path to the distribution folder containing the modules of the locally worked on plugins | `$pwd/dist` |
47+
| `CODESPACE_INTEGRATIONS` | Path to the custom components folder containing the files of the locally worked on integrations | `$pwd/custom_components` |
48+
| `CODESPACE_TEST` | Path to the test folder containing the configuration files for the test environment | `$pwd/test` |
49+
50+
### Mount Points
51+
52+
A `/workspace` volume is set up to make local files available in the container. More specifically, all test related files
53+
can be mounted in `/workspace/test`, all locally built javascript modules (plugins) can be mounted in `/workspace/dist`
54+
and all locally developed python modules (integrations) can be mounted in `/workspace/custom_components`.
55+
56+
Custom configuration files for Home Assistant should be placed inside the `config` folder of the test mount point.
57+
Note that the configuration file might be modified while launching the container, depending on the value of certain
58+
environment variables. To avoid unwanted modifications to your ogininal configuration file, the contents of
59+
`/workspace/test/config` are copied to `/config` before starting Home Assistant.
60+
Similarly the contents of the `/workspace/dist` folder are copied to `/config/www/workspace` and the contents of
61+
the `/workspace/custom_components` folder are copied to `/config/custom_components`.
62+
When the container script is launched, each mount point is continuously watched for any changes. These changes are
63+
then immediately reflected in Home Assistant's configuration folder.
64+
65+
**Note**: When working in a codespace, the active workspace files can be bound through the `CODESPACE_PLUGINS`,
66+
`CODESPACE_INTEGRATIONS` and `CODESPACE_TEST` environment variables.
3367

3468
### About Lovelace Plugins
35-
The dowload and install of plugins is *very* basic. This is not HACS.
69+
The dowload and installation of plugins is *very* basic. This is not HACS.
70+
71+
`LOVELACE_PLUGINS` should be a semicolon separated list of author/repo pairs, e.g. `thomasloven/lovelace-card-mod;kalkih/mini-media-player`
3672

37-
`LOVELACE_PLUGINS` should be a space separated list of author/repo pairs, e.g. `"thomasloven/lovelace-card-mod kalkih/mini-media-player"`
73+
Any locally developed javascript modules mounted in `/workspace/dist` are also installed.
3874

39-
`LOVELACE_LOCAL_FILES` is for the currently worked on plugins and should be a list of file names which are mounted in `/config/www/workspace`.
75+
### About Integrations
76+
If a locally developed integration is not manually configured in the test configuration, it is automatically added.
4077

4178
### Container Script
4279

@@ -60,13 +97,31 @@ Launch Home Assistant with `hass -c /config -v`
6097
```json
6198
{
6299
"image": "dcbr/hass-custom-devcontainer",
63-
"postCreateCommand": "container setup && npm add",
100+
"postCreateCommand": "sudo -E container setup && npm add",
101+
"postStartCommand": "sudo -E container launch",
64102
"forwardPorts": [8123],
65103
"mounts": [
66-
"source=${localWorkspaceFolder},target=/config/www/workspace,type=bind",
67-
"source=${localWorkspaceFolder}/test,target=/config/test,type=bind",
68-
"source=${localWorkspaceFolder}/test/configuration.yaml,target=/config/configuration.yaml,type=bind"
104+
"source=${localWorkspaceFolder},target=/workspace,type=bind,readonly"
69105
],
70-
"runArgs": ["--env-file", "${localWorkspaceFolder}/test/.env"]
106+
"runArgs": ["--env-file", "${localWorkspaceFolder}/test/test.env"]
107+
}
108+
```
109+
110+
## Github codespace example
111+
112+
```json
113+
{
114+
"image": "dcbr/hass-custom-devcontainer",
115+
"postCreateCommand": "sudo -E container setup && npm add",
116+
"postStartCommand": "sudo -E container launch",
117+
"forwardPorts": [8123],
118+
"containerEnv": {
119+
"CODESPACE_INTEGRATIONS": "${localWorkspaceFolder}/integration",
120+
"CODESPACE_PLUGINS": "${localWorkspaceFolder}/es",
121+
"CODESPACE_TEST": "${localWorkspaceFolder}/test",
122+
"ENV_FILE": "${localWorkspaceFolder}/test/test.env"
123+
}
71124
}
72-
```
125+
```
126+
127+
See also [dcbr/ha-templated-card](https://github.com/dcbr/ha-templated-card) for an example repository.

requirements.txt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
homeassistant==2022.12.9
2-
stdlib-list==0.8.0
3-
tqdm==4.65.0
4-
black==22.12.0
5-
flake8==4.0.1
1+
homeassistant==2024.12.0
2+
black==24.10.0
3+
flake8==7.1.1

scripts/install_plugins.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import uuid
2+
import json
3+
import os
4+
import pathlib
5+
import sys
6+
7+
def install_plugins(config_path, urls=None):
8+
# Load already installed plugins
9+
plugins = load_plugins(config_path)
10+
# Iterate over all local workspace modules and downloaded github modules
11+
local_plugins = fetch_local_plugins(config_path)
12+
for url, meta in local_plugins.items():
13+
if url not in plugins:
14+
# Add the local plugin if it is not already installed
15+
plugins[url] = {}
16+
# Update the timestamp of the local plugin
17+
plugins[url]['timestamp'] = meta['timestamp']
18+
# Delete local workspace modules that are no longer available
19+
deleted_plugins = set(plugins.keys()) - set(local_plugins.keys())
20+
for url in deleted_plugins:
21+
del plugins[url]
22+
# Add the passed module urls if they are not already installed
23+
if urls is not None:
24+
for url in urls:
25+
if url not in plugins:
26+
plugins[url] = {}
27+
# Save all plugins
28+
save_plugins(config_path, plugins)
29+
30+
def load_plugins(config_path):
31+
# Load the already installed plugins
32+
existing_plugins = {}
33+
try:
34+
with open(config_path / '.storage' / 'lovelace_resources', 'r') as f:
35+
res_data = json.load(f)
36+
for item in res_data['data']['items']:
37+
url, timestamp, *_ = item['url'].split('?t=') + [None]
38+
existing_plugins[url] = {
39+
'id': item['id'],
40+
'timestamp': timestamp,
41+
}
42+
except FileNotFoundError:
43+
pass
44+
return existing_plugins
45+
46+
def fetch_local_plugins(config_path):
47+
local_plugins = []
48+
# Fetch all local workspace modules
49+
local_files_path = config_path / 'www' / 'workspace'
50+
local_plugins.extend(local_files_path.rglob('*.js'))
51+
local_plugins.extend(local_files_path.rglob('*.mjs'))
52+
# Fetch all downloaded github modules
53+
github_files_path = config_path / 'www' / 'github'
54+
local_plugins.extend(github_files_path.glob('*.js'))
55+
# Retrieve timestamps (last modified time) and convert to relative paths
56+
local_plugins = {
57+
str('local' / plugin.relative_to(config_path / 'www')): {
58+
'timestamp': int(os.path.getmtime(plugin))
59+
}
60+
for plugin in local_plugins
61+
}
62+
return local_plugins
63+
64+
def save_plugins(config_path, plugins):
65+
# Build resources data structure
66+
res_data = {
67+
'version': 1,
68+
'minor_version': 1,
69+
'key': 'lovelace_resources',
70+
'data': {
71+
'items': [
72+
{
73+
'id': f"{meta.get('id', uuid.uuid4())!s}",
74+
'type': 'module',
75+
'url': f"{url}{'?t={}'.format(meta['timestamp']) if 'timestamp' in meta else ''}"
76+
} for url, meta in plugins.items()
77+
]
78+
}
79+
}
80+
# Write as json to storage
81+
with open(config_path / '.storage' / 'lovelace_resources', 'w') as f:
82+
json.dump(res_data, f, indent=4)
83+
84+
if __name__ == "__main__":
85+
install_plugins(pathlib.Path('/config'))

0 commit comments

Comments
 (0)