Allow stop/start home-assistant & flow of startup (#182)

* Allow config boot

* Read boot settings

* Use internal boot time for detect reboot

* Check if Home-Assistant need to watch

* Make datetime string and parse_datetime

* Add api calls

* fix lint p1

* Use new datetime parser for sessions and make a real default boot time

* fix lint p2

* only start docker if they is running

* convert to int (timestamp)

* add boot flag
This commit is contained in:
Pascal Vizeli 2017-09-11 14:14:26 +02:00 committed by GitHub
parent a733886803
commit 5a80be9fd4
12 changed files with 171 additions and 14 deletions

5
API.md
View File

@ -283,7 +283,8 @@ Optional:
"last_version": "LAST_VERSION",
"devices": [""],
"image": "str",
"custom": "bool -> if custom image"
"custom": "bool -> if custom image",
"boot": "bool"
}
```
@ -304,6 +305,8 @@ Output is the raw Docker log.
- POST `/homeassistant/restart`
- POST `/homeassistant/options`
- POST `/homeassistant/check`
- POST `/homeassistant/start`
- POST `/homeassistant/stop`
```json
{

View File

@ -72,6 +72,8 @@ class RestAPI(object):
self.webapp.router.add_post('/homeassistant/options', api_hass.options)
self.webapp.router.add_post('/homeassistant/update', api_hass.update)
self.webapp.router.add_post('/homeassistant/restart', api_hass.restart)
self.webapp.router.add_post('/homeassistant/stop', api_hass.stop)
self.webapp.router.add_post('/homeassistant/start', api_hass.start)
self.webapp.router.add_post('/homeassistant/check', api_hass.check)
def register_addons(self, addons):

View File

@ -7,14 +7,16 @@ import voluptuous as vol
from .util import api_process, api_process_raw, api_validate
from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_DEVICES, ATTR_IMAGE, ATTR_CUSTOM,
CONTENT_TYPE_BINARY)
ATTR_BOOT, CONTENT_TYPE_BINARY)
from ..validate import HASS_DEVICES
_LOGGER = logging.getLogger(__name__)
# pylint: disable=no-value-for-parameter
SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_DEVICES): HASS_DEVICES,
vol.Optional(ATTR_BOOT): vol.Boolean(),
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): vol.Any(None, vol.Coerce(str)),
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'):
vol.Any(None, vol.Coerce(str)),
@ -43,6 +45,7 @@ class APIHomeAssistant(object):
ATTR_IMAGE: self.homeassistant.image,
ATTR_DEVICES: self.homeassistant.devices,
ATTR_CUSTOM: self.homeassistant.is_custom_image,
ATTR_BOOT: self.homeassistant.boot,
}
@api_process
@ -57,6 +60,9 @@ class APIHomeAssistant(object):
self.homeassistant.set_custom(
body[ATTR_IMAGE], body[ATTR_LAST_VERSION])
if ATTR_BOOT in body:
self.homeassistant.boot = body[ATTR_BOOT]
return True
@api_process
@ -71,6 +77,16 @@ class APIHomeAssistant(object):
return await asyncio.shield(
self.homeassistant.update(version), loop=self.loop)
@api_process
def stop(self, request):
"""Stop homeassistant."""
return asyncio.shield(self.homeassistant.stop(), loop=self.loop)
@api_process
def start(self, request):
"""Start homeassistant."""
return asyncio.shield(self.homeassistant.run(), loop=self.loop)
@api_process
def restart(self, request):
"""Restart homeassistant."""

View File

@ -7,14 +7,12 @@ from pathlib import Path, PurePath
from .const import (
FILE_HASSIO_CONFIG, HASSIO_DATA, ATTR_SECURITY, ATTR_SESSIONS,
ATTR_PASSWORD, ATTR_TOTP, ATTR_TIMEZONE, ATTR_ADDONS_CUSTOM_LIST,
ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT)
from .tools import JsonConfig
ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT, ATTR_LAST_BOOT)
from .tools import JsonConfig, parse_datetime
from .validate import SCHEMA_HASSIO_CONFIG
_LOGGER = logging.getLogger(__name__)
DATETIME_FORMAT = "%Y%m%d %H:%M:%S"
HOMEASSISTANT_CONFIG = PurePath("homeassistant")
HASSIO_SSL = PurePath("ssl")
@ -28,6 +26,8 @@ BACKUP_DATA = PurePath("backup")
SHARE_DATA = PurePath("share")
TMP_DATA = PurePath("tmp")
DEFAULT_BOOT_TIME = datetime.utcfromtimestamp(0).isoformat()
class CoreConfig(JsonConfig):
"""Hold all core config data."""
@ -48,6 +48,22 @@ class CoreConfig(JsonConfig):
self._data[ATTR_TIMEZONE] = value
self.save()
@property
def last_boot(self):
"""Return last boot datetime."""
boot_str = self._data.get(ATTR_LAST_BOOT, DEFAULT_BOOT_TIME)
boot_time = parse_datetime(boot_str)
if not boot_time:
return datetime.utcfromtimestamp(1)
return boot_time
@last_boot.setter
def last_boot(self, value):
"""Set last boot datetime."""
self._data[ATTR_LAST_BOOT] = value.isoformat()
self.save()
@property
def path_hassio(self):
"""Return hassio data path."""
@ -191,14 +207,14 @@ class CoreConfig(JsonConfig):
def security_sessions(self):
"""Return api sessions."""
return {
session: datetime.strptime(until, DATETIME_FORMAT) for
session: parse_datetime(until) for
session, until in self._data[ATTR_SESSIONS].items()
}
def add_security_session(self, session, valid):
"""Set the a new session."""
self._data[ATTR_SESSIONS].update(
{session: valid.strftime(DATETIME_FORMAT)}
{session: valid.isoformat()}
)
self.save()

View File

@ -61,6 +61,7 @@ ATTR_SOURCE = 'source'
ATTR_FEATURES = 'features'
ATTR_ADDONS = 'addons'
ATTR_VERSION = 'version'
ATTR_LAST_BOOT = 'last_boot'
ATTR_LAST_VERSION = 'last_version'
ATTR_BETA_CHANNEL = 'beta_channel'
ATTR_NAME = 'name'

View File

@ -142,7 +142,7 @@ class HassIO(object):
try:
# HomeAssistant is already running / supervisor have only reboot
if await self.homeassistant.is_running():
if self.hardware.last_boot == self.config.last_boot:
_LOGGER.info("HassIO reboot detected")
return
@ -153,11 +153,15 @@ class HassIO(object):
await self.addons.auto_boot(STARTUP_SERVICES)
# run HomeAssistant
await self.homeassistant.run()
if self.homeassistant.boot:
await self.homeassistant.run()
# start addon mark as application
await self.addons.auto_boot(STARTUP_APPLICATION)
# store new last boot
self.config.last_boot = self.hardware.last_boot
finally:
# schedule homeassistant watchdog
self.scheduler.register_task(

View File

@ -1,6 +1,8 @@
"""Init file for HassIO docker object."""
import logging
import docker
from .interface import DockerInterface
_LOGGER = logging.getLogger(__name__)
@ -93,3 +95,19 @@ class DockerHomeAssistant(DockerInterface):
{'bind': '/ssl', 'mode': 'ro'},
}
)
def is_initialize(self):
"""Return True if docker container exists."""
return self.loop.run_in_executor(None, self._is_initialize)
def _is_initialize(self):
"""Return True if docker container exists.
Need run inside executor.
"""
try:
self.docker.containers.get(self.name)
except docker.errors.DockerException:
return False
return True

View File

@ -1,4 +1,5 @@
"""Read hardware info from system."""
from datetime import datetime
import logging
from pathlib import Path
import re
@ -15,6 +16,9 @@ RE_CARDS = re.compile(r"(\d+) \[(\w*) *\]: (.*\w)")
ASOUND_DEVICES = Path("/proc/asound/devices")
RE_DEVICES = re.compile(r"\[.*(\d+)- (\d+).*\]: ([\w ]*)")
PROC_STAT = Path("/proc/stat")
RE_BOOT_TIME = re.compile(r"btime (\d+)")
class Hardware(object):
"""Represent a interface to procfs, sysfs and udev."""
@ -85,3 +89,21 @@ class Hardware(object):
continue
return audio_list
@property
def last_boot(self):
"""Return last boot time."""
try:
with PROC_STAT.open("r") as stat_file:
stats = stat_file.read()
except OSError as err:
_LOGGER.error("Can't read stat data -> %s", err)
return
# parse stat file
found = RE_BOOT_TIME.search(stats)
if not found:
_LOGGER.error("Can't found last boot time!")
return
return datetime.utcfromtimestamp(int(found.group(1)))

View File

@ -6,7 +6,7 @@ import re
from .const import (
FILE_HASSIO_HOMEASSISTANT, ATTR_DEVICES, ATTR_IMAGE, ATTR_LAST_VERSION,
ATTR_VERSION)
ATTR_VERSION, ATTR_BOOT)
from .dock.homeassistant import DockerHomeAssistant
from .tools import JsonConfig, convert_to_ascii
from .validate import SCHEMA_HASS_CONFIG
@ -73,6 +73,17 @@ class HomeAssistant(JsonConfig):
self._data[ATTR_DEVICES] = value
self.save()
@property
def boot(self):
"""Return True if home-assistant boot is enabled."""
return self._data[ATTR_BOOT]
@boot.setter
def boot(self, value):
"""Set home-assistant boot options."""
self._data[ATTR_BOOT] = value
self.save()
def set_custom(self, image, version):
"""Set a custom image for homeassistant."""
# reset
@ -119,6 +130,7 @@ class HomeAssistant(JsonConfig):
async def update(self, version=None):
"""Update HomeAssistant version."""
version = version or self.last_version
running = await self.docker.is_running()
if version == self.docker.version:
_LOGGER.warning("Version %s is already installed", version)
@ -127,7 +139,8 @@ class HomeAssistant(JsonConfig):
try:
return await self.docker.update(version)
finally:
await self.docker.run()
if running:
await self.docker.run()
def run(self):
"""Run HomeAssistant docker.
@ -164,6 +177,13 @@ class HomeAssistant(JsonConfig):
"""
return self.docker.is_running()
def is_initialize(self):
"""Return True if a docker container is exists.
Return a coroutine.
"""
return self.docker.is_initialize()
@property
def in_progress(self):
"""Return True if a task is in progress."""

View File

@ -66,6 +66,11 @@ def homeassistant_watchdog(loop, homeassistant):
"""Create scheduler task for montoring running state."""
async def _homeassistant_watchdog():
"""Check running state and start if they is close."""
# if Home-Assistant is active
if not await homeassistant.is_initialize():
return
# If Home-Assistant is running
if homeassistant.in_progress or await homeassistant.is_running():
return

View File

@ -1,13 +1,14 @@
"""Tools file for HassIO."""
import asyncio
from contextlib import suppress
from datetime import datetime
from datetime import datetime, timedelta, timezone
import json
import logging
import re
import aiohttp
import async_timeout
import pytz
import voluptuous as vol
from voluptuous.humanize import humanize_error
@ -17,6 +18,16 @@ FREEGEOIP_URL = "https://freegeoip.io/json/"
RE_STRING = re.compile(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))")
# Copyright (c) Django Software Foundation and individual contributors.
# All rights reserved.
# https://github.com/django/django/blob/master/LICENSE
DATETIME_RE = re.compile(
r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})'
r'[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})'
r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?'
r'(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$'
)
def write_json_file(jsonfile, data):
"""Write a json file."""
@ -53,6 +64,42 @@ def convert_to_ascii(raw):
return RE_STRING.sub("", raw.decode())
# Copyright (c) Django Software Foundation and individual contributors.
# All rights reserved.
# https://github.com/django/django/blob/master/LICENSE
def parse_datetime(dt_str):
"""Parse a string and return a datetime.datetime.
This function supports time zone offsets. When the input contains one,
the output uses a timezone with a fixed offset from UTC.
Raises ValueError if the input is well formatted but not a valid datetime.
Returns None if the input isn't well formatted.
"""
match = DATETIME_RE.match(dt_str)
if not match:
return None
kws = match.groupdict() # type: Dict[str, Any]
if kws['microsecond']:
kws['microsecond'] = kws['microsecond'].ljust(6, '0')
tzinfo_str = kws.pop('tzinfo')
tzinfo = None # type: Optional[dt.tzinfo]
if tzinfo_str == 'Z':
tzinfo = pytz.utc
elif tzinfo_str is not None:
offset_mins = int(tzinfo_str[-2:]) if len(tzinfo_str) > 3 else 0
offset_hours = int(tzinfo_str[1:3])
offset = timedelta(hours=offset_hours, minutes=offset_mins)
if tzinfo_str[0] == '-':
offset = -offset
tzinfo = timezone(offset)
else:
tzinfo = None
kws = {k: int(v) for k, v in kws.items() if v is not None}
kws['tzinfo'] = tzinfo
return datetime(**kws)
class JsonConfig(object):
"""Hass core object for handle it."""

View File

@ -7,7 +7,7 @@ from .const import (
ATTR_DEVICES, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_SESSIONS, ATTR_PASSWORD,
ATTR_TOTP, ATTR_SECURITY, ATTR_BETA_CHANNEL, ATTR_TIMEZONE,
ATTR_ADDONS_CUSTOM_LIST, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT,
ATTR_HOMEASSISTANT, ATTR_HASSIO)
ATTR_HOMEASSISTANT, ATTR_HASSIO, ATTR_BOOT, ATTR_LAST_BOOT)
NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
@ -55,8 +55,10 @@ DOCKER_PORTS = vol.Schema({
})
# pylint: disable=no-value-for-parameter
SCHEMA_HASS_CONFIG = vol.Schema({
vol.Optional(ATTR_DEVICES, default=[]): HASS_DEVICES,
vol.Optional(ATTR_BOOT, default=True): vol.Boolean(),
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): vol.Coerce(str),
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str),
})
@ -73,6 +75,7 @@ SCHEMA_UPDATER_CONFIG = vol.Schema({
# pylint: disable=no-value-for-parameter
SCHEMA_HASSIO_CONFIG = vol.Schema({
vol.Optional(ATTR_TIMEZONE, default='UTC'): validate_timezone,
vol.Optional(ATTR_LAST_BOOT): vol.Coerce(str),
vol.Optional(ATTR_ADDONS_CUSTOM_LIST, default=[]): [vol.Url()],
vol.Optional(ATTR_SECURITY, default=False): vol.Boolean(),
vol.Optional(ATTR_TOTP): vol.Coerce(str),