mirror of https://github.com/home-assistant/core
245 lines
7.5 KiB
Python
245 lines
7.5 KiB
Python
"""Support for monitoring the Syncthing instance."""
|
|
|
|
import aiosyncthing
|
|
|
|
from homeassistant.components.sensor import SensorEntity
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.exceptions import PlatformNotReady
|
|
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
from homeassistant.helpers.event import async_track_time_interval
|
|
|
|
from .const import (
|
|
DOMAIN,
|
|
FOLDER_PAUSED_RECEIVED,
|
|
FOLDER_SUMMARY_RECEIVED,
|
|
SCAN_INTERVAL,
|
|
SERVER_AVAILABLE,
|
|
SERVER_UNAVAILABLE,
|
|
STATE_CHANGED_RECEIVED,
|
|
)
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
config_entry: ConfigEntry,
|
|
async_add_entities: AddEntitiesCallback,
|
|
) -> None:
|
|
"""Set up the Syncthing sensors."""
|
|
syncthing = hass.data[DOMAIN][config_entry.entry_id]
|
|
|
|
try:
|
|
config = await syncthing.system.config()
|
|
version = await syncthing.system.version()
|
|
except aiosyncthing.exceptions.SyncthingError as exception:
|
|
raise PlatformNotReady from exception
|
|
|
|
server_id = syncthing.server_id
|
|
entities = [
|
|
FolderSensor(
|
|
syncthing,
|
|
server_id,
|
|
folder["id"],
|
|
folder["label"],
|
|
version["version"],
|
|
)
|
|
for folder in config["folders"]
|
|
]
|
|
|
|
async_add_entities(entities)
|
|
|
|
|
|
class FolderSensor(SensorEntity):
|
|
"""A Syncthing folder sensor."""
|
|
|
|
_attr_should_poll = False
|
|
_attr_translation_key = "syncthing"
|
|
|
|
STATE_ATTRIBUTES = {
|
|
"errors": "errors",
|
|
"globalBytes": "global_bytes",
|
|
"globalDeleted": "global_deleted",
|
|
"globalDirectories": "global_directories",
|
|
"globalFiles": "global_files",
|
|
"globalSymlinks": "global_symlinks",
|
|
"globalTotalItems": "global_total_items",
|
|
"ignorePatterns": "ignore_patterns",
|
|
"inSyncBytes": "in_sync_bytes",
|
|
"inSyncFiles": "in_sync_files",
|
|
"invalid": "invalid",
|
|
"localBytes": "local_bytes",
|
|
"localDeleted": "local_deleted",
|
|
"localDirectories": "local_directories",
|
|
"localFiles": "local_files",
|
|
"localSymlinks": "local_symlinks",
|
|
"localTotalItems": "local_total_items",
|
|
"needBytes": "need_bytes",
|
|
"needDeletes": "need_deletes",
|
|
"needDirectories": "need_directories",
|
|
"needFiles": "need_files",
|
|
"needSymlinks": "need_symlinks",
|
|
"needTotalItems": "need_total_items",
|
|
"pullErrors": "pull_errors",
|
|
"state": "state",
|
|
}
|
|
|
|
def __init__(self, syncthing, server_id, folder_id, folder_label, version):
|
|
"""Initialize the sensor."""
|
|
self._syncthing = syncthing
|
|
self._server_id = server_id
|
|
self._folder_id = folder_id
|
|
self._folder_label = folder_label
|
|
self._state = None
|
|
self._unsub_timer = None
|
|
|
|
self._short_server_id = server_id.split("-")[0]
|
|
self._attr_name = f"{self._short_server_id} {folder_id} {folder_label}"
|
|
self._attr_unique_id = f"{self._short_server_id}-{folder_id}"
|
|
self._attr_device_info = DeviceInfo(
|
|
entry_type=DeviceEntryType.SERVICE,
|
|
identifiers={(DOMAIN, self._server_id)},
|
|
manufacturer="Syncthing Team",
|
|
name=f"Syncthing ({syncthing.url})",
|
|
sw_version=version,
|
|
)
|
|
|
|
@property
|
|
def native_value(self):
|
|
"""Return the state of the sensor."""
|
|
return self._state["state"]
|
|
|
|
@property
|
|
def available(self):
|
|
"""Could the device be accessed during the last update call."""
|
|
return self._state is not None
|
|
|
|
@property
|
|
def extra_state_attributes(self):
|
|
"""Return the state attributes."""
|
|
return self._state
|
|
|
|
async def async_update_status(self):
|
|
"""Request folder status and update state."""
|
|
try:
|
|
state = await self._syncthing.database.status(self._folder_id)
|
|
except aiosyncthing.exceptions.SyncthingError:
|
|
self._state = None
|
|
else:
|
|
self._state = self._filter_state(state)
|
|
self.async_write_ha_state()
|
|
|
|
def subscribe(self):
|
|
"""Start polling syncthing folder status."""
|
|
if self._unsub_timer is None:
|
|
|
|
async def refresh(event_time):
|
|
"""Get the latest data from Syncthing."""
|
|
await self.async_update_status()
|
|
|
|
self._unsub_timer = async_track_time_interval(
|
|
self.hass, refresh, SCAN_INTERVAL
|
|
)
|
|
|
|
@callback
|
|
def unsubscribe(self):
|
|
"""Stop polling syncthing folder status."""
|
|
if self._unsub_timer is not None:
|
|
self._unsub_timer()
|
|
self._unsub_timer = None
|
|
|
|
async def async_added_to_hass(self) -> None:
|
|
"""Handle entity which will be added."""
|
|
|
|
@callback
|
|
def handle_folder_summary(event):
|
|
if self._state is not None:
|
|
self._state = self._filter_state(event["data"]["summary"])
|
|
self.async_write_ha_state()
|
|
|
|
self.async_on_remove(
|
|
async_dispatcher_connect(
|
|
self.hass,
|
|
f"{FOLDER_SUMMARY_RECEIVED}-{self._server_id}-{self._folder_id}",
|
|
handle_folder_summary,
|
|
)
|
|
)
|
|
|
|
@callback
|
|
def handle_state_changed(event):
|
|
if self._state is not None:
|
|
self._state["state"] = event["data"]["to"]
|
|
self.async_write_ha_state()
|
|
|
|
self.async_on_remove(
|
|
async_dispatcher_connect(
|
|
self.hass,
|
|
f"{STATE_CHANGED_RECEIVED}-{self._server_id}-{self._folder_id}",
|
|
handle_state_changed,
|
|
)
|
|
)
|
|
|
|
@callback
|
|
def handle_folder_paused(event):
|
|
if self._state is not None:
|
|
self._state["state"] = "paused"
|
|
self.async_write_ha_state()
|
|
|
|
self.async_on_remove(
|
|
async_dispatcher_connect(
|
|
self.hass,
|
|
f"{FOLDER_PAUSED_RECEIVED}-{self._server_id}-{self._folder_id}",
|
|
handle_folder_paused,
|
|
)
|
|
)
|
|
|
|
@callback
|
|
def handle_server_unavailable():
|
|
self._state = None
|
|
self.unsubscribe()
|
|
self.async_write_ha_state()
|
|
|
|
self.async_on_remove(
|
|
async_dispatcher_connect(
|
|
self.hass,
|
|
f"{SERVER_UNAVAILABLE}-{self._server_id}",
|
|
handle_server_unavailable,
|
|
)
|
|
)
|
|
|
|
async def handle_server_available():
|
|
self.subscribe()
|
|
await self.async_update_status()
|
|
|
|
self.async_on_remove(
|
|
async_dispatcher_connect(
|
|
self.hass,
|
|
f"{SERVER_AVAILABLE}-{self._server_id}",
|
|
handle_server_available,
|
|
)
|
|
)
|
|
|
|
self.subscribe()
|
|
self.async_on_remove(self.unsubscribe)
|
|
|
|
await self.async_update_status()
|
|
|
|
def _filter_state(self, state):
|
|
# Select only needed state attributes and map their names
|
|
state = {
|
|
self.STATE_ATTRIBUTES[key]: value
|
|
for key, value in state.items()
|
|
if key in self.STATE_ATTRIBUTES
|
|
}
|
|
|
|
# A workaround, for some reason, state of paused folders is an empty string
|
|
if state["state"] == "":
|
|
state["state"] = "paused"
|
|
|
|
# Add some useful attributes
|
|
state["id"] = self._folder_id
|
|
state["label"] = self._folder_label
|
|
|
|
return state
|