702d080a1e
Google BK
190 lines
8.8 KiB
Python
190 lines
8.8 KiB
Python
from datetime import timedelta
|
|
|
|
from aiohttp.client_exceptions import ClientResponseError
|
|
from injector import inject, singleton
|
|
|
|
from ..model import Coordinator, Backup
|
|
from ..config import Config, Setting
|
|
from ..util import GlobalInfo, Backoff, Estimator
|
|
from .harequests import HaRequests
|
|
from ..time import Time
|
|
from ..worker import Worker
|
|
from ..const import SOURCE_HA, SOURCE_GOOGLE_DRIVE
|
|
from ..logger import getLogger
|
|
|
|
logger = getLogger(__name__)
|
|
|
|
NOTIFICATION_TITLE = "Home Assistant Google Drive Backup is Having Trouble"
|
|
NOTIFICATION_DESC_LINK = "The add-on is having trouble making backups and needs attention. Please visit the add-on [status page]({0}) for details."
|
|
NOTIFICATION_DESC_STATIC = "The add-on is having trouble making backups and needs attention. Please visit the add-on status page for details."
|
|
|
|
MAX_BACKOFF = 60 * 5 # 5 minutes
|
|
FIRST_BACKOFF = 60 # 1 minute
|
|
|
|
# Wait 5 minutes before logging
|
|
NOTIFY_DELAY = 60 * 5 # 5 minute
|
|
|
|
OLD_BACKUP_ENTITY_NAME = "sensor.snapshot_backup"
|
|
BACKUP_ENTITY_NAME = "sensor.backup_state"
|
|
|
|
REASSURING_MESSAGE = "Unable to reach Home Assistant (HTTP {0}). This is normal if Home Assistant is restarting. You will probably see some errors in the supervisor logs until it comes back online."
|
|
|
|
|
|
@singleton
|
|
class HaUpdater(Worker):
|
|
@inject
|
|
def __init__(self, requests: HaRequests, coordinator: Coordinator, config: Config, time: Time, global_info: GlobalInfo):
|
|
self._config = config
|
|
super().__init__("Sensor Updater", self.update, time, self.getInterval)
|
|
self._time = time
|
|
self._coordinator = coordinator
|
|
self._requests: HaRequests = requests
|
|
self._info = global_info
|
|
self._notified = False
|
|
self._backoff = Backoff(max=MAX_BACKOFF, base=FIRST_BACKOFF)
|
|
self._first_error = None
|
|
self._trigger_once = False
|
|
|
|
self._last_backup_update = None
|
|
self.last_backup_update_time = time.now() - timedelta(days=1)
|
|
self._config.subscribe(self.config_updated)
|
|
self._last_interval = self.getInterval()
|
|
|
|
def config_updated(self):
|
|
if self._last_interval != self.getInterval():
|
|
self._wait_event.set()
|
|
self._last_interval = self.getInterval()
|
|
|
|
def getInterval(self):
|
|
return self._config.get(Setting.HA_REPORTING_INTERVAL_SECONDS)
|
|
|
|
async def update(self):
|
|
try:
|
|
if self._config.get(Setting.ENABLE_BACKUP_STALE_SENSOR):
|
|
await self._requests.updateBackupStaleSensor('on' if self._stale() else 'off')
|
|
if self._config.get(Setting.ENABLE_BACKUP_STATE_SENSOR):
|
|
await self._maybeSendBackupUpdate()
|
|
if self._config.get(Setting.NOTIFY_FOR_STALE_BACKUPS):
|
|
if self._stale() and not self._notified:
|
|
if self._info.url is None or len(self._info.url) == 0:
|
|
message = NOTIFICATION_DESC_STATIC
|
|
else:
|
|
message = NOTIFICATION_DESC_LINK.format(self._info.url)
|
|
await self._requests.sendNotification(NOTIFICATION_TITLE, message)
|
|
self._notified = True
|
|
elif not self._stale() and self._notified:
|
|
await self._requests.dismissNotification()
|
|
self._notified = False
|
|
self._backoff.reset()
|
|
self._first_error = None
|
|
self._trigger_once = False
|
|
except ClientResponseError as e:
|
|
if self._first_error is None:
|
|
self._first_error = self._time.now()
|
|
if int(e.status / 100) == 5:
|
|
if self._time.now() > self._first_error + timedelta(seconds=NOTIFY_DELAY):
|
|
logger.error(
|
|
"Unable to reach Home Assistant (HTTP {0}). This is normal if Home Assistant is restarting. You will probably see some errors in the supervisor logs until it comes back online.".format(e.status))
|
|
else:
|
|
logger.error("Trouble updating Home Assistant sensors.")
|
|
self._last_backup_update = None
|
|
await self._time.sleepAsync(self._backoff.backoff(e))
|
|
except Exception as e:
|
|
self._last_backup_update = None
|
|
logger.error("Trouble updating Home Assistant sensors.")
|
|
logger.printException(e)
|
|
await self._time.sleepAsync(self._backoff.backoff(e))
|
|
|
|
async def _maybeSendBackupUpdate(self):
|
|
update = self._buildBackupUpdate()
|
|
if self._trigger_once or update != self._last_backup_update or self._time.now() > self.last_backup_update_time + timedelta(hours=1):
|
|
if self._config.get(Setting.CALL_BACKUP_SNAPSHOT):
|
|
await self._requests.updateEntity(OLD_BACKUP_ENTITY_NAME, update)
|
|
else:
|
|
await self._requests.updateEntity(BACKUP_ENTITY_NAME, update)
|
|
self._last_backup_update = update
|
|
self.last_backup_update_time = self._time.now()
|
|
|
|
def _stale(self):
|
|
if self._info._first_sync:
|
|
return False
|
|
if self._info._last_error:
|
|
return self._time.now() > self._info._last_success + timedelta(seconds=self._config.get(Setting.BACKUP_STALE_SECONDS))
|
|
else:
|
|
next_backup = self._coordinator.nextBackupTime(include_pending=False)
|
|
if not next_backup:
|
|
# no backups are configured
|
|
return False
|
|
|
|
# Determine if a lot of time has passed since the last backup "should" have been made.
|
|
warn_after = next_backup + timedelta(seconds=self._config.get(Setting.LONG_TERM_STALE_BACKUP_SECONDS))
|
|
return self._time.now() >= warn_after
|
|
|
|
def _state(self):
|
|
if self._stale():
|
|
return "error"
|
|
else:
|
|
return "waiting" if self._info._first_sync else "backed_up"
|
|
|
|
def triggerRefresh(self):
|
|
self._trigger_once = True
|
|
|
|
def _buildBackupUpdate(self):
|
|
backups = list(filter(lambda s: not s.ignore(), self._coordinator.backups()))
|
|
last = "Never"
|
|
if len(backups) > 0:
|
|
last = max(backups, key=lambda s: s.date()).date().isoformat()
|
|
|
|
def makeBackupData(backup: Backup):
|
|
return {
|
|
"name": backup.name(),
|
|
"date": str(backup.date().isoformat()),
|
|
"state": backup.status(),
|
|
"size": backup.sizeString(),
|
|
"slug": backup.slug()
|
|
}
|
|
ha_backups = list(filter(lambda s: s.getSource(SOURCE_HA) is not None, backups))
|
|
drive_backups = list(filter(lambda s: s.getSource(SOURCE_GOOGLE_DRIVE) is not None, backups))
|
|
|
|
last_uploaded = "Never"
|
|
if len(drive_backups) > 0:
|
|
last_uploaded = max(drive_backups, key=lambda s: s.date()).date().isoformat()
|
|
if self._config.get(Setting.CALL_BACKUP_SNAPSHOT):
|
|
return {
|
|
"state": self._state(),
|
|
"attributes": {
|
|
"friendly_name": "Snapshot State",
|
|
"last_snapshot": last, # type: ignore
|
|
"snapshots_in_google_drive": len(drive_backups),
|
|
"snapshots_in_hassio": len(ha_backups),
|
|
"snapshots_in_home_assistant": len(ha_backups),
|
|
"size_in_google_drive": Estimator.asSizeString(sum(map(lambda v: v.sizeInt(), drive_backups))),
|
|
"size_in_home_assistant": Estimator.asSizeString(sum(map(lambda v: v.sizeInt(), ha_backups))),
|
|
"snapshots": list(map(makeBackupData, backups))
|
|
}
|
|
}
|
|
else:
|
|
source_metrics = self._coordinator.buildBackupMetrics()
|
|
next = self._coordinator.nextBackupTime()
|
|
if next is not None:
|
|
next = next.isoformat()
|
|
attr = {
|
|
"friendly_name": "Backup State",
|
|
"last_backup": last, # type: ignore
|
|
"next_backup": next,
|
|
"last_uploaded": last_uploaded,
|
|
"backups_in_google_drive": len(drive_backups),
|
|
"backups_in_home_assistant": len(ha_backups),
|
|
"size_in_google_drive": Estimator.asSizeString(sum(map(lambda v: v.sizeInt(), drive_backups))),
|
|
"size_in_home_assistant": Estimator.asSizeString(sum(map(lambda v: v.sizeInt(), ha_backups))),
|
|
"backups": list(map(makeBackupData, backups))
|
|
}
|
|
if SOURCE_GOOGLE_DRIVE in source_metrics and 'free_space' in source_metrics[SOURCE_GOOGLE_DRIVE]:
|
|
attr["free_space_in_google_drive"] = source_metrics[SOURCE_GOOGLE_DRIVE]['free_space']
|
|
else:
|
|
attr["free_space_in_google_drive"] = ""
|
|
return {
|
|
"state": self._state(),
|
|
"attributes": attr
|
|
}
|