mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-05-20 10:34:30 +00:00
homeassistant: Add the most popular app for home automation
- Use docker container via registry.freedombox.org to obtain the package. Specify this in the description. - Mark the app as experimental. - Show information that a dedicated domain is required to host Home Assistant. - Use special YAML loader/dumper to deal with custom YAML tags in configuration file. - Obtain logo file from a test file in code repository with Apache license as the actual logo files are freely licensed. - Write functional tests without accessing the website as a dedicated domain is necessary. Tests: - Functional tests work. - Add a domain 'mydomain.example' using the Names app. Assign this domain in Home Assistant app configuration. In /etc/hosts on the host machine add a mapping from mydomain.example to the IP address of the container/VM. Access the web interface using https://mydomain.example. Home Assistant web interface is available and functional. - After install of the app the configuration.yaml file contains the proxy related lines are expected. - Diagnostics work (expect the URL access). - Re-run setup works. - 'Launch web client' and frontpage shortcut work as expected. - Non-admin users can't connect on port 8123. - Home Assistant is able to establish websocket connection in its web UI. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
f83485b68c
commit
ce341b18ab
6
debian/copyright
vendored
6
debian/copyright
vendored
@ -124,6 +124,12 @@ Files: static/themes/default/icons/gnu-linux.png
|
|||||||
Copyright: 2017 Cowemoji
|
Copyright: 2017 Cowemoji
|
||||||
License: CC0-1.0
|
License: CC0-1.0
|
||||||
|
|
||||||
|
Files: plinth/modules/homeassistant/static/icons/homeassistant.png
|
||||||
|
plinth/modules/homeassistant/static/icons/homeassistant.svg
|
||||||
|
Copyright: Home Assistant Core Developers
|
||||||
|
Comment: https://github.com/home-assistant/core/blob/dev/tests/components/image_upload/logo.png
|
||||||
|
License: Apache-2.0
|
||||||
|
|
||||||
Files: plinth/modules/ikiwiki/static/icons/ikiwiki.png
|
Files: plinth/modules/ikiwiki/static/icons/ikiwiki.png
|
||||||
plinth/modules/ikiwiki/static/icons/ikiwiki.svg
|
plinth/modules/ikiwiki/static/icons/ikiwiki.svg
|
||||||
Copyright: 2006 Recai Oktaş <roktas@debian.org>
|
Copyright: 2006 Recai Oktaş <roktas@debian.org>
|
||||||
|
|||||||
141
plinth/modules/homeassistant/__init__.py
Normal file
141
plinth/modules/homeassistant/__init__.py
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""FreedomBox app to configure Home Assistant."""
|
||||||
|
|
||||||
|
from django.utils.functional import lazy
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from plinth import app as app_module
|
||||||
|
from plinth import cfg, frontpage, menu
|
||||||
|
from plinth.config import DropinConfigs
|
||||||
|
from plinth.container import Container
|
||||||
|
from plinth.modules.apache.components import WebserverRoot
|
||||||
|
from plinth.modules.backups.components import BackupRestore
|
||||||
|
from plinth.modules.firewall.components import (Firewall,
|
||||||
|
FirewallLocalProtection)
|
||||||
|
from plinth.package import Packages
|
||||||
|
from plinth.utils import format_lazy
|
||||||
|
|
||||||
|
from . import manifest, privileged
|
||||||
|
|
||||||
|
_alert = '''
|
||||||
|
<div class="alert alert-warning d-flex align-items-center" role="alert">
|
||||||
|
<div class="me-2">
|
||||||
|
<span class="fa fa-exclamation-triangle" aria-hidden="true"></span>
|
||||||
|
<span class="visually-hidden">{}</span>
|
||||||
|
</div>
|
||||||
|
<div>{}</div>
|
||||||
|
</div>
|
||||||
|
'''
|
||||||
|
|
||||||
|
_description = [
|
||||||
|
_('Home Assistant is a home automation hub with emphasis on local control '
|
||||||
|
'and privacy. It integrates with thousands of devices including smart '
|
||||||
|
'bulbs, alarms, presense sensors, door bells, thermostats, irrigation '
|
||||||
|
'timers, energy monitors, etc.'),
|
||||||
|
_('Home Assistant can detect, configure, and use various devices on the '
|
||||||
|
'local network. For devices using other protocols such as ZigBee, it '
|
||||||
|
'typically requires additional hardware such as a ZigBee USB dongle. '
|
||||||
|
'You need to re-run setup if such hardware is added or removed.'),
|
||||||
|
_('Home Assistant web interface must be setup soon after the app is '
|
||||||
|
'installed. An administrator account is created at this time. Home '
|
||||||
|
'Assistant maintains its own user accounts.'),
|
||||||
|
format_lazy(
|
||||||
|
_('Please note that Home Assistant is installed and run inside a '
|
||||||
|
'container provided by the Home Assistant project. Security, '
|
||||||
|
'quality, privacy and legal reviews are done by the upstream '
|
||||||
|
'project and not by Debian/{box_name}. Updates are performed '
|
||||||
|
'following an independent cycle.'), box_name=_(cfg.box_name)),
|
||||||
|
format_lazy(_alert, _('Caution:'), _('This app is experimental.')),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class HomeAssistnatApp(app_module.App):
|
||||||
|
"""FreedomBox app for Home Assistant."""
|
||||||
|
|
||||||
|
app_id = 'homeassistant'
|
||||||
|
|
||||||
|
_version = 1
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Create components for the app."""
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
info = app_module.Info(app_id=self.app_id, version=self._version,
|
||||||
|
name=_('Home Assistant'),
|
||||||
|
icon_filename='homeassistant',
|
||||||
|
description=_description,
|
||||||
|
manual_page='HomeAssistant',
|
||||||
|
clients=manifest.clients, tags=manifest.tags)
|
||||||
|
self.add(info)
|
||||||
|
|
||||||
|
menu_item = menu.Menu('menu-homeassistant', info.name,
|
||||||
|
info.icon_filename, info.tags,
|
||||||
|
'homeassistant:index', parent_url_name='apps')
|
||||||
|
self.add(menu_item)
|
||||||
|
|
||||||
|
shortcut = frontpage.Shortcut('shortcut-homeassistant', info.name,
|
||||||
|
icon=info.icon_filename, url='#',
|
||||||
|
clients=info.clients, tags=info.tags,
|
||||||
|
login_required=True)
|
||||||
|
self.add(shortcut)
|
||||||
|
|
||||||
|
packages = Packages('packages-homeassistant', ['podman'],
|
||||||
|
conflicts=['libpam-tmpdir'],
|
||||||
|
conflicts_action=Packages.ConflictsAction.REMOVE)
|
||||||
|
self.add(packages)
|
||||||
|
|
||||||
|
dropin_configs = DropinConfigs('dropin-configs-homeassistant', [
|
||||||
|
'/etc/apache2/includes/home-assistant-freedombox.conf',
|
||||||
|
])
|
||||||
|
self.add(dropin_configs)
|
||||||
|
|
||||||
|
firewall = Firewall('firewall-homeassistant', info.name,
|
||||||
|
ports=['http', 'https'], is_external=True)
|
||||||
|
self.add(firewall)
|
||||||
|
|
||||||
|
firewall_local_protection = FirewallLocalProtection(
|
||||||
|
'firewall-local-protection-homeassistant', ['8123'])
|
||||||
|
self.add(firewall_local_protection)
|
||||||
|
|
||||||
|
webserver = WebserverRoot('webserverroot-homeassistant',
|
||||||
|
'home-assistant-freedombox')
|
||||||
|
self.add(webserver)
|
||||||
|
|
||||||
|
image_name = 'registry.freedombox.org/' \
|
||||||
|
'homeassistant/home-assistant:stable'
|
||||||
|
volume_name = 'home-assistant-freedombox'
|
||||||
|
volumes = {
|
||||||
|
'/run/dbus': '/run/dbus',
|
||||||
|
volume_name: '/config',
|
||||||
|
}
|
||||||
|
devices = {
|
||||||
|
f'/dev/ttyUSB{number}': f'/dev/ttyUSB{number}'
|
||||||
|
for number in range(8)
|
||||||
|
}
|
||||||
|
container = Container(
|
||||||
|
'container-homeassistant', 'home-assistant-freedombox',
|
||||||
|
image_name=image_name, volume_name=volume_name,
|
||||||
|
volume_path='/var/lib/home-assistant-freedombox/config/',
|
||||||
|
volumes=volumes, devices=devices, listen_ports=[(8123, 'tcp4')])
|
||||||
|
self.add(container)
|
||||||
|
|
||||||
|
backup_restore = BackupRestore('backup-restore-homeassistant',
|
||||||
|
**manifest.backup)
|
||||||
|
self.add(backup_restore)
|
||||||
|
|
||||||
|
def post_init(self):
|
||||||
|
"""Perform post initialization operations."""
|
||||||
|
root = self.get_component('webserverroot-homeassistant')
|
||||||
|
|
||||||
|
def get_url():
|
||||||
|
return f'https://{root.domain_get()}'
|
||||||
|
|
||||||
|
url = lazy(get_url, str)()
|
||||||
|
self.get_component('shortcut-homeassistant').url = url
|
||||||
|
self.info.clients[0]['platforms'][0]['url'] = url
|
||||||
|
|
||||||
|
def setup(self, old_version):
|
||||||
|
"""Install and configure the app."""
|
||||||
|
super().setup(old_version)
|
||||||
|
|
||||||
|
privileged.setup()
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
##
|
||||||
|
## On one site, provide Home Assistant on a default path: /
|
||||||
|
##
|
||||||
|
|
||||||
|
# Handle WebSocket connections using websocket proxy
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteCond %{HTTP:Upgrade} websocket [NC]
|
||||||
|
RewriteRule ^/(api/websocket)$ "ws://127.0.0.1:8123/$1" [P,L]
|
||||||
|
|
||||||
|
# Proxy all requests to Home Assistant
|
||||||
|
ProxyPass / http://127.0.0.1:8123/
|
||||||
@ -0,0 +1 @@
|
|||||||
|
plinth.modules.homeassistant
|
||||||
68
plinth/modules/homeassistant/manifest.py
Normal file
68
plinth/modules/homeassistant/manifest.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""Application manifest for Home Assistant."""
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from plinth.clients import store_url
|
||||||
|
|
||||||
|
_android_package_id = 'io.homeassistant.companion.android'
|
||||||
|
|
||||||
|
clients = [
|
||||||
|
{
|
||||||
|
'name': _('Home Assistant'),
|
||||||
|
'platforms': [{
|
||||||
|
'type': 'web',
|
||||||
|
'url': '#' # Filled in later
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name':
|
||||||
|
_('Home Assistant'),
|
||||||
|
'platforms': [{
|
||||||
|
'type': 'download',
|
||||||
|
'os': 'macos',
|
||||||
|
'url': 'https://apps.apple.com/us/app/home-assistant/id1099568401'
|
||||||
|
}, {
|
||||||
|
'type': 'store',
|
||||||
|
'os': 'ios',
|
||||||
|
'store_name': 'app-store',
|
||||||
|
'url': 'https://apps.apple.com/us/app/home-assistant/id1099568401'
|
||||||
|
'?platform=iphone'
|
||||||
|
}, {
|
||||||
|
'type':
|
||||||
|
'store',
|
||||||
|
'os':
|
||||||
|
'android',
|
||||||
|
'store_name':
|
||||||
|
'google-play',
|
||||||
|
'url':
|
||||||
|
store_url('google-play', 'io.homeassistant.companion.android')
|
||||||
|
}, {
|
||||||
|
'type':
|
||||||
|
'store',
|
||||||
|
'os':
|
||||||
|
'android',
|
||||||
|
'store_name':
|
||||||
|
'f-droid',
|
||||||
|
'url':
|
||||||
|
store_url('f-droid',
|
||||||
|
'io.homeassistant.companion.android.minimal')
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
backup = {
|
||||||
|
'data': {
|
||||||
|
'directories': ['/var/lib/home-assistant-freedombox/']
|
||||||
|
},
|
||||||
|
'services': ['home-assistant-freedombox']
|
||||||
|
}
|
||||||
|
|
||||||
|
tags = [
|
||||||
|
_('Home Automation'),
|
||||||
|
_('IoT'),
|
||||||
|
_('Wi-Fi'),
|
||||||
|
_('ZigBee'),
|
||||||
|
_('Z-Wave'),
|
||||||
|
_('Thread')
|
||||||
|
]
|
||||||
104
plinth/modules/homeassistant/privileged.py
Normal file
104
plinth/modules/homeassistant/privileged.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""Configure Home Assistant."""
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import pathlib
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from plinth.actions import privileged
|
||||||
|
|
||||||
|
_settings_file = pathlib.Path(
|
||||||
|
'/var/lib/home-assistant-freedombox/config/configuration.yaml')
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class YAMLUnknownTag:
|
||||||
|
"""Object used to represent an unknown tag in YAML."""
|
||||||
|
tag: str
|
||||||
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
class YAMLLoader(yaml.SafeLoader):
|
||||||
|
"""Custom YAML loader to handle !include etc. tags."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def yaml_unknown_constructor(loader, node, tag):
|
||||||
|
"""Create an object when a unknown tag is encountered."""
|
||||||
|
value = loader.construct_scalar(node)
|
||||||
|
return YAMLUnknownTag(tag, value)
|
||||||
|
|
||||||
|
|
||||||
|
class YAMLDumper(yaml.Dumper):
|
||||||
|
"""Custom YAML dumper to handle !include etc. tags."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def yaml_unknown_representor(dumper, data):
|
||||||
|
"""Dump original tag from an object representing an unknown tag."""
|
||||||
|
return dumper.represent_scalar(data.tag, data.value)
|
||||||
|
|
||||||
|
|
||||||
|
def yaml_add_handlers():
|
||||||
|
"""Add special handlers to YAML loader and dumper."""
|
||||||
|
tags = [
|
||||||
|
'!include', '!env_var', '!secret', '!include_dir_list',
|
||||||
|
'!include_dir_merge_list', '!include_dir_named',
|
||||||
|
'!include_dir_merge_named', '!input'
|
||||||
|
]
|
||||||
|
for tag in tags:
|
||||||
|
YAMLLoader.add_constructor(
|
||||||
|
tag, functools.partial(yaml_unknown_constructor, tag=tag))
|
||||||
|
|
||||||
|
YAMLDumper.add_representer(YAMLUnknownTag, yaml_unknown_representor)
|
||||||
|
|
||||||
|
|
||||||
|
yaml_add_handlers()
|
||||||
|
|
||||||
|
|
||||||
|
@privileged
|
||||||
|
def setup() -> None:
|
||||||
|
"""Setup basic Home Assistant configuration."""
|
||||||
|
pathlib.Path('/var/lib/home-assistant-freedombox/').chmod(0o700)
|
||||||
|
|
||||||
|
try:
|
||||||
|
_wait_for_configuration_file()
|
||||||
|
|
||||||
|
settings = _read_settings()
|
||||||
|
if 'http' not in settings:
|
||||||
|
settings['http'] = {}
|
||||||
|
|
||||||
|
settings['http']['server_host'] = '127.0.0.1'
|
||||||
|
settings['http']['use_x_forwarded_for'] = True
|
||||||
|
settings['http']['trusted_proxies'] = '127.0.0.1'
|
||||||
|
_write_settings(settings)
|
||||||
|
except Exception as exception:
|
||||||
|
raise Exception(
|
||||||
|
traceback.format_tb(exception.__traceback__) +
|
||||||
|
[_settings_file.read_text()])
|
||||||
|
|
||||||
|
|
||||||
|
def _wait_for_configuration_file() -> None:
|
||||||
|
"""Wait until the Home Assistant daemon creates a configuration file."""
|
||||||
|
start_time = time.time()
|
||||||
|
while time.time() < start_time + 300:
|
||||||
|
if _settings_file.exists():
|
||||||
|
break
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_settings() -> dict:
|
||||||
|
"""Load settings as dictionary from YAML config file."""
|
||||||
|
with _settings_file.open('rb') as settings_file:
|
||||||
|
return yaml.load(settings_file, Loader=YAMLLoader)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_settings(settings: dict):
|
||||||
|
"""Write settings from dictionary to YAML config file."""
|
||||||
|
with _settings_file.open('w', encoding='utf-8') as settings_file:
|
||||||
|
yaml.dump(settings, settings_file, Dumper=YAMLDumper)
|
||||||
BIN
plinth/modules/homeassistant/static/icons/homeassistant.png
Normal file
BIN
plinth/modules/homeassistant/static/icons/homeassistant.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
189
plinth/modules/homeassistant/static/icons/homeassistant.svg
Normal file
189
plinth/modules/homeassistant/static/icons/homeassistant.svg
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
id="svg2"
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
sodipodi:docname="homeassistant.svg"
|
||||||
|
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||||
|
inkscape:export-filename="homeassistant.png"
|
||||||
|
inkscape:export-xdpi="48"
|
||||||
|
inkscape:export-ydpi="48"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs6">
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="fillet_chamfer"
|
||||||
|
id="path-effect10481"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
nodesatellites_param="F,0,0,1,0,0,0,16 @ F,0,0,1,0,7.4006832,0,16 @ F,0,0,1,0,6.8476415,0,16 @ F,0,0,1,0,0,0,16 @ F,0,0,1,0,13,0,16 @ F,0,0,1,0,0,0,16 @ F,0,0,1,0,6.8398548,0,16 @ F,0,0,1,0,7.0625983,0,16 @ F,0,0,1,0,0,0,16 @ F,0,0,1,0,5.7657568,0,16 @ F,0,0,1,0,13,0,16"
|
||||||
|
unit="px"
|
||||||
|
method="auto"
|
||||||
|
mode="C"
|
||||||
|
radius="13"
|
||||||
|
chamfer_steps="1"
|
||||||
|
flexible="false"
|
||||||
|
use_knot_distance="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
apply_with_radius="true"
|
||||||
|
only_selected="false"
|
||||||
|
hide_knots="false" />
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview4"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="1.4142136"
|
||||||
|
inkscape:cx="109.9551"
|
||||||
|
inkscape:cy="229.1026"
|
||||||
|
inkscape:window-width="2054"
|
||||||
|
inkscape:window-height="1424"
|
||||||
|
inkscape:window-x="26"
|
||||||
|
inkscape:window-y="23"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="g8" />
|
||||||
|
<g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
inkscape:label="Image"
|
||||||
|
id="g8">
|
||||||
|
<g
|
||||||
|
id="g10867"
|
||||||
|
transform="matrix(1.4584376,0,0,1.4559519,-35.756104,-35.922376)">
|
||||||
|
<rect
|
||||||
|
style="display:inline;fill:#3db7ed;fill-opacity:1;stroke:none;stroke-width:6;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect10535"
|
||||||
|
width="351.06061"
|
||||||
|
height="351.65997"
|
||||||
|
x="24.516718"
|
||||||
|
y="24.672777"
|
||||||
|
rx="27.427588"
|
||||||
|
ry="27.427588" />
|
||||||
|
<path
|
||||||
|
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 93.540138,214.62714 -0.703144,95.63584 a 7.3637398,7.3637398 45.277884 0 0 7.346256,7.41786 l 202.50305,0.47543 a 6.8319137,6.8319137 135.06864 0 0 6.84795,-6.83156 l 0.005,-96.58845 28.86224,0.19532 a 5.2986741,5.2986741 112.56308 0 0 3.76544,-9.06234 l -33.64783,-33.34198 0.25133,-44.31303 a 6.8858444,6.8858444 45.516933 0 0 -6.80054,-6.92437 l -19.3796,-0.23979 a 6.9883849,6.9883849 135.40629 0 0 -7.07484,6.97521 l -0.0213,11.78286 -69.96618,-70.708974 a 5.7338785,5.7338785 0.14370144 0 0 -8.13135,-0.02039 L 60.681075,205.86618 a 5.3083747,5.3083747 67.196859 0 0 3.809347,9.06069 z"
|
||||||
|
id="path2919"
|
||||||
|
sodipodi:nodetypes="cccccccccccc"
|
||||||
|
inkscape:original-d="m 93.540138,214.62714 -0.757555,103.03632 216.751337,0.50889 0.005,-103.43609 41.86194,0.2833 -42.88209,-42.4923 0.29012,-51.15277 -33.28099,-0.4118 -0.0341,18.84545 L 201.4722,65.00069 51.491114,215.061 Z"
|
||||||
|
inkscape:path-effect="#path-effect10481" />
|
||||||
|
<g
|
||||||
|
id="g10604">
|
||||||
|
<ellipse
|
||||||
|
style="fill:none;stroke:#3db7ed;stroke-width:4.9;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path326"
|
||||||
|
cx="179.24469"
|
||||||
|
cy="149.3372"
|
||||||
|
rx="7.3252597"
|
||||||
|
ry="7.3252592" />
|
||||||
|
<ellipse
|
||||||
|
style="fill:none;stroke:#3db7ed;stroke-width:4.9;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path326-1"
|
||||||
|
cx="169.45244"
|
||||||
|
cy="190.98294"
|
||||||
|
rx="7.3252597"
|
||||||
|
ry="7.3252592" />
|
||||||
|
<ellipse
|
||||||
|
style="fill:none;stroke:#3db7ed;stroke-width:4.9;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path326-2"
|
||||||
|
cx="138.19283"
|
||||||
|
cy="222.78395"
|
||||||
|
rx="7.3252597"
|
||||||
|
ry="7.3252592" />
|
||||||
|
<ellipse
|
||||||
|
style="fill:none;stroke:#3db7ed;stroke-width:4.9;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path326-70"
|
||||||
|
cx="141.91083"
|
||||||
|
cy="257.76157"
|
||||||
|
rx="7.3252597"
|
||||||
|
ry="7.3252592" />
|
||||||
|
<ellipse
|
||||||
|
style="fill:none;stroke:#3db7ed;stroke-width:4.9;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path326-9"
|
||||||
|
cx="179.11179"
|
||||||
|
cy="262.82785"
|
||||||
|
rx="7.3252597"
|
||||||
|
ry="7.3252592" />
|
||||||
|
<ellipse
|
||||||
|
style="fill:none;stroke:#3db7ed;stroke-width:4.9;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path326-36"
|
||||||
|
cx="147.65073"
|
||||||
|
cy="294.24802"
|
||||||
|
rx="7.3252597"
|
||||||
|
ry="7.3252592" />
|
||||||
|
<ellipse
|
||||||
|
style="fill:none;stroke:#3db7ed;stroke-width:4.9;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path326-0"
|
||||||
|
cx="255.81784"
|
||||||
|
cy="294.59372"
|
||||||
|
rx="7.3252597"
|
||||||
|
ry="7.3252592" />
|
||||||
|
<ellipse
|
||||||
|
style="fill:none;stroke:#3db7ed;stroke-width:4.9;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path326-6"
|
||||||
|
cx="274.44196"
|
||||||
|
cy="259.56049"
|
||||||
|
rx="7.3252597"
|
||||||
|
ry="7.3252592" />
|
||||||
|
<ellipse
|
||||||
|
style="fill:none;stroke:#3db7ed;stroke-width:4.9;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path326-26"
|
||||||
|
cx="225.69614"
|
||||||
|
cy="227.99767"
|
||||||
|
rx="7.3252597"
|
||||||
|
ry="7.3252592" />
|
||||||
|
<ellipse
|
||||||
|
style="fill:none;stroke:#3db7ed;stroke-width:4.9;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path326-18"
|
||||||
|
cx="251.68121"
|
||||||
|
cy="165.4055"
|
||||||
|
rx="7.3252597"
|
||||||
|
ry="7.3252592" />
|
||||||
|
<ellipse
|
||||||
|
style="fill:none;stroke:#3db7ed;stroke-width:4.9;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path326-3"
|
||||||
|
cx="223.96227"
|
||||||
|
cy="149.19836"
|
||||||
|
rx="7.3252597"
|
||||||
|
ry="7.3252592" />
|
||||||
|
<ellipse
|
||||||
|
style="fill:none;stroke:#3db7ed;stroke-width:4.9;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path326-7"
|
||||||
|
cx="229.48242"
|
||||||
|
cy="192.50766"
|
||||||
|
rx="7.3252597"
|
||||||
|
ry="7.3252592" />
|
||||||
|
<ellipse
|
||||||
|
style="fill:none;stroke:#3db7ed;stroke-width:4.9;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path326-3-5"
|
||||||
|
cx="274.20001"
|
||||||
|
cy="192.36882"
|
||||||
|
rx="7.3252597"
|
||||||
|
ry="7.3252592" />
|
||||||
|
<ellipse
|
||||||
|
style="fill:none;stroke:#3db7ed;stroke-width:4.9;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path326-35"
|
||||||
|
cx="142.36525"
|
||||||
|
cy="164.90781"
|
||||||
|
rx="7.3252597"
|
||||||
|
ry="7.3252592" />
|
||||||
|
<path
|
||||||
|
id="path2657"
|
||||||
|
style="fill:none;stroke:#3db7ed;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 251.49286,172.81947 0.0666,61.7281 -49.7452,49.80448 m 66.78444,-86.57997 -16.96839,16.54133 -15.64318,-15.88293 m 30.5797,60.84935 -40.48194,0.22401 -0.14847,-22.94487 m 23.03301,58.54101 -24.716,0.0115 -22.8132,22.58867 m 17.48042,-163.52317 -17.1326,16.68666 -0.37143,145.80773 -54.16934,-54.41329 m 32.00458,8.05924 -0.0525,23.92802 -23.86244,-0.18309 m 45.55819,-40.76105 -58.50429,-57.16861 -0.0519,-22.53777 m 27.22317,25.92054 0.24132,23.39815 -24.00093,0.0152 m 39.09799,-67.98579 16.75747,16.12059"
|
||||||
|
sodipodi:nodetypes="ccccccccccccccccccccccccccc" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 7.7 KiB |
43
plinth/modules/homeassistant/templates/homeassistant.html
Normal file
43
plinth/modules/homeassistant/templates/homeassistant.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{% extends "app.html" %}
|
||||||
|
{% comment %}
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% load bootstrap %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block configuration %}
|
||||||
|
{% if form %}
|
||||||
|
<h3>{% trans "Configuration" %}</h3>
|
||||||
|
|
||||||
|
<form id="app-form" class="form form-configuration" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Home Assistant requires a dedicated domain to work with and cannot
|
||||||
|
work on a URL path. Please select the domain on which Home Assistant
|
||||||
|
will be available. Home Assistant will not be available on other
|
||||||
|
domains.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% url 'names:domain-add' as names_url %}
|
||||||
|
{% url 'dynamicdns:index' as dynamic_dns_url %}
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
A separate domain can be made available for Home Assistant by
|
||||||
|
configurating a subdomain such as homeassistant.mydomain.example. See
|
||||||
|
<a href="{{ names_url }}">Names</a> app and
|
||||||
|
<a href="{{ dynamic_dns_url }}">Dynamic DNS</a> app for configuring
|
||||||
|
subdomains.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{ form|bootstrap }}
|
||||||
|
|
||||||
|
<input type="submit" class="btn btn-primary"
|
||||||
|
value="{% trans "Update setup" %}"/>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
0
plinth/modules/homeassistant/tests/__init__.py
Normal file
0
plinth/modules/homeassistant/tests/__init__.py
Normal file
28
plinth/modules/homeassistant/tests/test_functional.py
Normal file
28
plinth/modules/homeassistant/tests/test_functional.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""Functional, browser based tests for Home Assistant app."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from plinth.tests import functional
|
||||||
|
|
||||||
|
pytestmark = [pytest.mark.apps, pytest.mark.homeassistant]
|
||||||
|
|
||||||
|
|
||||||
|
class TestHomeAssitantApp(functional.BaseAppTests):
|
||||||
|
"""Basic tests for Home Assistant app."""
|
||||||
|
app_name = 'homeassistant'
|
||||||
|
has_service = True
|
||||||
|
has_web = False # Can't yet check separate sub-domain
|
||||||
|
diagnostics_delay = 5
|
||||||
|
|
||||||
|
def install_and_setup(self, session_browser):
|
||||||
|
"""Set the domain to freedombox.local so that it can tested."""
|
||||||
|
super().install_and_setup(session_browser)
|
||||||
|
_domain_set(session_browser, 'freedombox.local')
|
||||||
|
|
||||||
|
|
||||||
|
def _domain_set(browser, domain):
|
||||||
|
"""Set the domain in the domain selection drop down."""
|
||||||
|
functional.nav_to_module(browser, 'homeassistant')
|
||||||
|
browser.select('domain_name', domain)
|
||||||
|
functional.submit(browser, form_class='form-configuration')
|
||||||
11
plinth/modules/homeassistant/urls.py
Normal file
11
plinth/modules/homeassistant/urls.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""URLs for the Home Assistant module."""
|
||||||
|
|
||||||
|
from django.urls import re_path
|
||||||
|
|
||||||
|
from .views import HomeAssistantAppView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
re_path(r'^apps/homeassistant/$', HomeAssistantAppView.as_view(),
|
||||||
|
name='index')
|
||||||
|
]
|
||||||
55
plinth/modules/homeassistant/views.py
Normal file
55
plinth/modules/homeassistant/views.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""Django views for Home Assistant app."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from plinth.forms import DomainSelectionForm
|
||||||
|
from plinth.views import AppView
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class HomeAssistantAppView(AppView):
|
||||||
|
"""Show Home Assistant app main view."""
|
||||||
|
|
||||||
|
app_id = 'homeassistant'
|
||||||
|
template_name = 'homeassistant.html'
|
||||||
|
form_class = DomainSelectionForm
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
"""Return the values to fill in the form."""
|
||||||
|
initial = super().get_initial()
|
||||||
|
component = self.app.get_component('webserverroot-homeassistant')
|
||||||
|
initial.update({
|
||||||
|
'domain_name': component.domain_get() or '',
|
||||||
|
})
|
||||||
|
return initial
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
"""Return the arguments to instantiate form with."""
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs['show_none'] = True
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
"""Apply the changes submitted in the form."""
|
||||||
|
old_config = self.get_initial()
|
||||||
|
new_config = form.cleaned_data
|
||||||
|
|
||||||
|
is_changed = False
|
||||||
|
|
||||||
|
def _value_changed(key):
|
||||||
|
return old_config.get(key) != new_config.get(key)
|
||||||
|
|
||||||
|
if _value_changed('domain_name'):
|
||||||
|
component = self.app.get_component('webserverroot-homeassistant')
|
||||||
|
component.domain_set(new_config['domain_name'] or None)
|
||||||
|
is_changed = True
|
||||||
|
|
||||||
|
if is_changed:
|
||||||
|
messages.success(self.request, _('Configuration updated.'))
|
||||||
|
|
||||||
|
return super().form_valid(form)
|
||||||
Loading…
x
Reference in New Issue
Block a user