Skip to content

Commit a028f11

Browse files
authored
Change handling of addon config (#12)
Add an optional extended description…
1 parent 4edd02c commit a028f11

File tree

8 files changed

+109
-66
lines changed

8 files changed

+109
-66
lines changed

API.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ On success
3737
"slug": "xy",
3838
"version": "CURRENT_VERSION",
3939
"installed": "none|INSTALL_VERSION",
40+
"dedicated": "bool",
4041
"description": "description"
4142
}
4243
]
@@ -146,7 +147,10 @@ Output the raw docker log
146147

147148
- `/addons/{addon}/options`
148149
```json
149-
{ }
150+
{
151+
"boot": "auto|manual",
152+
"options": {},
153+
}
150154
```
151155

152156
- `/addons/{addon}/start`

hassio/addons/__init__.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,8 @@ async def reload(self):
4545
self.read_addons_repo()
4646

4747
# remove stalled addons
48-
tasks = []
4948
for addon in self.list_removed:
50-
_LOGGER.info("Old addon %s found")
51-
tasks.append(self.loop.create_task(self.uninstall(addon)))
52-
53-
if tasks:
54-
await asyncio.wait(tasks, loop=self.loop)
49+
_LOGGER.warning("Dedicated addon '%s' found!", addon)
5550

5651
async def auto_boot(self, start_type):
5752
"""Boot addons with mode auto."""
@@ -88,7 +83,7 @@ async def install(self, addon, version=None):
8883
return False
8984

9085
self.dockers[addon] = addon_docker
91-
self.set_install_addon(addon, version)
86+
self.set_addon_install(addon, version)
9287
return True
9388

9489
async def uninstall(self, addon):
@@ -110,7 +105,7 @@ async def uninstall(self, addon):
110105
shutil.rmtree(self.path_data(addon))
111106

112107
self.dockers.pop(addon)
113-
self.set_uninstall_addon(addon)
108+
self.set_addon_uninstall(addon)
114109
return True
115110

116111
async def state(self, addon):
@@ -150,8 +145,13 @@ async def update(self, addon, version=None):
150145
return False
151146

152147
version = version or self.get_version(addon)
148+
is_running = self.dockers[addon].is_running()
149+
150+
# update
153151
if await self.dockers[addon].update(version):
154-
self.set_version(addon, version)
152+
self.set_addon_update(addon, version)
153+
if is_running:
154+
await self.start(addon)
155155
return True
156156
return False
157157

hassio/addons/data.py

Lines changed: 62 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010
FILE_HASSIO_ADDONS, ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON,
1111
ATTR_STARTUP, ATTR_BOOT, ATTR_MAP_SSL, ATTR_MAP_CONFIG, ATTR_OPTIONS,
1212
ATTR_PORTS, BOOT_AUTO, DOCKER_REPO, ATTR_INSTALLED, ATTR_SCHEMA,
13-
ATTR_IMAGE)
13+
ATTR_IMAGE, ATTR_DEDICATED)
1414
from ..config import Config
1515
from ..tools import read_json_file, write_json_file
1616

1717
_LOGGER = logging.getLogger(__name__)
1818

1919
ADDONS_REPO_PATTERN = "{}/*/config.json"
20+
SYSTEM = "system"
21+
USER = "user"
2022

2123

2224
class AddonsData(Config):
@@ -26,12 +28,22 @@ def __init__(self, config):
2628
"""Initialize data holder."""
2729
super().__init__(FILE_HASSIO_ADDONS)
2830
self.config = config
29-
self._addons_data = {}
31+
self._addons_data = self._data.get(SYSTEM, {})
32+
self._user_data = self._data.get(USER, {})
33+
self._current_data = {}
3034
self.arch = None
3135

36+
def save(self):
37+
"""Store data to config file."""
38+
self._data = {
39+
USER: self._user_data,
40+
SYSTEM: self._addons_data,
41+
}
42+
super().save()
43+
3244
def read_addons_repo(self):
3345
"""Read data from addons repository."""
34-
self._addons_data = {}
46+
self._current_data = {}
3547

3648
self._read_addons_folder(self.config.path_addons_repo)
3749
self._read_addons_folder(self.config.path_addons_custom)
@@ -45,7 +57,7 @@ def _read_addons_folder(self, folder):
4557
addon_config = read_json_file(addon)
4658

4759
addon_config = SCHEMA_ADDON_CONFIG(addon_config)
48-
self._addons_data[addon_config[ATTR_SLUG]] = addon_config
60+
self._current_data[addon_config[ATTR_SLUG]] = addon_config
4961

5062
except (OSError, KeyError):
5163
_LOGGER.warning("Can't read %s", addon)
@@ -57,32 +69,33 @@ def _read_addons_folder(self, folder):
5769
@property
5870
def list_installed(self):
5971
"""Return a list of installed addons."""
60-
return set(self._data.keys())
61-
62-
@property
63-
def list_all(self):
64-
"""Return a list of available addons."""
6572
return set(self._addons_data.keys())
6673

6774
@property
6875
def list(self):
6976
"""Return a list of available addons."""
7077
data = []
71-
for addon, values in self._addons_data.items():
78+
all_addons = {**self._addons_data, **self._current_data}
79+
dedicated = self.list_removed
80+
81+
for addon, values in all_addons.items():
82+
i_version = self._addons_data.get(addon, {}).get(ATTR_VERSION)
83+
7284
data.append({
7385
ATTR_NAME: values[ATTR_NAME],
7486
ATTR_SLUG: values[ATTR_SLUG],
7587
ATTR_DESCRIPTON: values[ATTR_DESCRIPTON],
7688
ATTR_VERSION: values[ATTR_VERSION],
77-
ATTR_INSTALLED: self._data.get(addon, {}).get(ATTR_VERSION),
89+
ATTR_INSTALLED: i_version,
90+
ATTR_DEDICATED: addon in dedicated,
7891
})
7992

8093
return data
8194

8295
def list_startup(self, start_type):
8396
"""Get list of installed addon with need start by type."""
8497
addon_list = set()
85-
for addon in self._data.keys():
98+
for addon in self._addons_data.keys():
8699
if self.get_boot(addon) != BOOT_AUTO:
87100
continue
88101

@@ -99,58 +112,64 @@ def list_startup(self, start_type):
99112
def list_removed(self):
100113
"""Return local addons they not support from repo."""
101114
addon_list = set()
102-
for addon in self._data.keys():
103-
if addon not in self._addons_data:
115+
for addon in self._addons_data.keys():
116+
if addon not in self._current_data:
104117
addon_list.add(addon)
105118

106119
return addon_list
107120

108121
def exists_addon(self, addon):
109122
"""Return True if a addon exists."""
110-
return addon in self._addons_data
123+
return addon in self._current_data or addon in self._addons_data
111124

112125
def is_installed(self, addon):
113126
"""Return True if a addon is installed."""
114-
return addon in self._data
127+
return addon in self._addons_data
115128

116129
def version_installed(self, addon):
117130
"""Return installed version."""
118-
return self._data[addon][ATTR_VERSION]
131+
return self._addons_data[addon][ATTR_VERSION]
119132

120-
def set_install_addon(self, addon, version):
133+
def set_addon_install(self, addon, version):
121134
"""Set addon as installed."""
122-
self._data[addon] = {
123-
ATTR_VERSION: version,
124-
ATTR_OPTIONS: {}
135+
self._addons_data[addon] = self._current_data[addon]
136+
self._user_data[addon] = {
137+
ATTR_OPTIONS: {},
125138
}
126139
self.save()
127140

128-
def set_uninstall_addon(self, addon):
141+
def set_addon_uninstall(self, addon):
129142
"""Set addon as uninstalled."""
130-
self._data.pop(addon, None)
143+
self._addons_data.pop(addon, None)
144+
self._user_data.pop(addon, None)
145+
self.save()
146+
147+
def set_addon_update(self, addon, version):
148+
"""Update version of addon."""
149+
self._addons_data[addon] = self._current_data[addon]
131150
self.save()
132151

133152
def set_options(self, addon, options):
134153
"""Store user addon options."""
135-
self._data[addon][ATTR_OPTIONS] = options
154+
self._user_data[addon][ATTR_OPTIONS] = options
136155
self.save()
137156

138-
def set_version(self, addon, version):
139-
"""Update version of addon."""
140-
self._data[addon][ATTR_VERSION] = version
157+
def set_boot(self, addon, boot):
158+
"""Store user boot options."""
159+
self._user_data[addon][ATTR_BOOT] = boot
141160
self.save()
142161

143162
def get_options(self, addon):
144163
"""Return options with local changes."""
145-
opt = self._addons_data[addon][ATTR_OPTIONS]
146-
if addon in self._data:
147-
opt.update(self._data[addon][ATTR_OPTIONS])
148-
return opt
164+
return {
165+
**self._addons_data[addon][ATTR_OPTIONS],
166+
**self._user_data[addon][ATTR_OPTIONS],
167+
}
149168

150169
def get_boot(self, addon):
151170
"""Return boot config with prio local settings."""
152-
if ATTR_BOOT in self._data[addon]:
153-
return self._data[addon][ATTR_BOOT]
171+
if ATTR_BOOT in self._user_data[addon]:
172+
return self._user_data[addon][ATTR_BOOT]
154173

155174
return self._addons_data[addon][ATTR_BOOT]
156175

@@ -164,23 +183,22 @@ def get_description(self, addon):
164183

165184
def get_version(self, addon):
166185
"""Return version of addon."""
167-
return self._addons_data[addon][ATTR_VERSION]
168-
169-
def get_slug(self, addon):
170-
"""Return slug of addon."""
171-
return self._addons_data[addon][ATTR_SLUG]
186+
if addon not in self._current_data:
187+
return self.version_installed(addon)
188+
return self._current_data[addon][ATTR_VERSION]
172189

173190
def get_ports(self, addon):
174191
"""Return ports of addon."""
175192
return self._addons_data[addon].get(ATTR_PORTS)
176193

177194
def get_image(self, addon):
178195
"""Return image name of addon."""
179-
if ATTR_IMAGE not in self._addons_data[addon]:
180-
return "{}/{}-addon-{}".format(
181-
DOCKER_REPO, self.arch, self.get_slug(addon))
196+
addon_data = self._addons_data.get(addon, self._current_data[addon])
197+
198+
if ATTR_IMAGE not in addon_data:
199+
return "{}/{}-addon-{}".format(DOCKER_REPO, self.arch, addon)
182200

183-
return self._addons_data[addon][ATTR_IMAGE]
201+
return addon_data[ATTR_IMAGE]
184202

185203
def need_config(self, addon):
186204
"""Return True if config map is needed."""
@@ -192,13 +210,11 @@ def need_ssl(self, addon):
192210

193211
def path_data(self, addon):
194212
"""Return addon data path inside supervisor."""
195-
return "{}/{}".format(
196-
self.config.path_addons_data, self._addons_data[addon][ATTR_SLUG])
213+
return "{}/{}".format(self.config.path_addons_data, addon)
197214

198215
def path_data_docker(self, addon):
199216
"""Return addon data path external for docker."""
200-
return "{}/{}".format(self.config.path_addons_data_docker,
201-
self._addons_data[addon][ATTR_SLUG])
217+
return "{}/{}".format(self.config.path_addons_data_docker, addon)
202218

203219
def path_addon_options(self, addon):
204220
"""Return path to addons options."""

hassio/api/addons.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,18 @@
88
from .util import api_process, api_process_raw, api_validate
99
from ..const import (
1010
ATTR_VERSION, ATTR_CURRENT, ATTR_STATE, ATTR_BOOT, ATTR_OPTIONS,
11-
STATE_STOPPED, STATE_STARTED)
11+
STATE_STOPPED, STATE_STARTED, BOOT_AUTO, BOOT_MANUAL)
1212

1313
_LOGGER = logging.getLogger(__name__)
1414

1515
SCHEMA_VERSION = vol.Schema({
1616
vol.Optional(ATTR_VERSION): vol.Coerce(str),
1717
})
1818

19+
SCHEMA_OPTIONS = vol.Schema({
20+
vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL])
21+
})
22+
1923

2024
class APIAddons(object):
2125
"""Handle rest api for addons functions."""
@@ -56,10 +60,19 @@ async def info(self, request):
5660
async def options(self, request):
5761
"""Store user options for addon."""
5862
addon = self._extract_addon(request)
59-
schema = self.addons.get_schema(addon)
63+
options_schema = self.addons.get_schema(addon)
64+
65+
addon_schema = SCHEMA_OPTIONS.extend({
66+
vol.Optional(ATTR_OPTIONS): options_schema,
67+
})
68+
69+
addon_config = await api_validate(addon_schema, request)
70+
71+
if ATTR_OPTIONS in addon_config:
72+
self.addons.set_options(addon, addon_config[ATTR_OPTIONS])
73+
if ATTR_BOOT in addon_config:
74+
self.addons.set_options(addon, addon_config[ATTR_BOOT])
6075

61-
options = await api_validate(schema, request)
62-
self.addons.set_options(addon, options)
6376
return True
6477

6578
@api_process

hassio/const.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Const file for HassIO."""
2-
HASSIO_VERSION = '0.12'
2+
HASSIO_VERSION = '0.13'
33

44
URL_HASSIO_VERSION = \
55
'https://raw.githubusercontent.com/pvizeli/hassio/master/version.json'
@@ -45,6 +45,7 @@
4545
ATTR_MAP_SSL = 'map_ssl'
4646
ATTR_OPTIONS = 'options'
4747
ATTR_INSTALLED = 'installed'
48+
ATTR_DEDICATED = 'dedicated'
4849
ATTR_STATE = 'state'
4950
ATTR_SCHEMA = 'schema'
5051
ATTR_IMAGE = 'image'

hassio/dock/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,6 @@ def _update(self, tag):
223223
224224
Need run inside executor.
225225
"""
226-
old_run = self._is_running()
227226
old_image = "{}:{}".format(self.image, self.version)
228227

229228
_LOGGER.info("Update docker %s with %s:%s",
@@ -238,9 +237,6 @@ def _update(self, tag):
238237
except docker.errors.DockerException as err:
239238
_LOGGER.warning(
240239
"Can't remove old image %s -> %s", old_image, err)
241-
# restore
242-
if old_run:
243-
self._run()
244240
return True
245241

246242
return False

hassio/dock/addon.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def __init__(self, config, loop, dock, addons_data, addon):
2424
@property
2525
def docker_name(self):
2626
"""Return name of docker container."""
27-
return "addon_{}".format(self.addons_data.get_slug(self.addon))
27+
return "addon_{}".format(self.addon)
2828

2929
def _run(self):
3030
"""Run docker image.

hassio/dock/homeassistant.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,16 @@ def _run(self):
6262
return False
6363

6464
return True
65+
66+
async def update(self, tag):
67+
"""Update homeassistant docker image."""
68+
if self._lock.locked():
69+
_LOGGER.error("Can't excute update while a task is in progress")
70+
return False
71+
72+
async with self._lock:
73+
if await self.loop.run_in_executor(None, self._update, tag):
74+
await self.loop.run_in_executor(None, self._run)
75+
return True
76+
77+
return False

0 commit comments

Comments
 (0)