core/homeassistant/components/syncthing/sensor.py

269 lines
8.0 KiB
Python

"""Support for monitoring the Syncthing instance."""
import logging
import aiosyncthing
from homeassistant.components.sensor import SensorEntity
from homeassistant.core import callback
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.event import async_track_time_interval
from .const import (
DOMAIN,
FOLDER_PAUSED_RECEIVED,
FOLDER_SENSOR_ALERT_ICON,
FOLDER_SENSOR_DEFAULT_ICON,
FOLDER_SENSOR_ICONS,
FOLDER_SUMMARY_RECEIVED,
SCAN_INTERVAL,
SERVER_AVAILABLE,
SERVER_UNAVAILABLE,
STATE_CHANGED_RECEIVED,
)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""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."""
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._version = version
self._short_server_id = server_id.split("-")[0]
@property
def name(self):
"""Return the name of the sensor."""
return f"{self._short_server_id} {self._folder_id} {self._folder_label}"
@property
def unique_id(self):
"""Return the unique id of the entity."""
return f"{self._short_server_id}-{self._folder_id}"
@property
def state(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 icon(self):
"""Return the icon for this sensor."""
if self._state is None:
return FOLDER_SENSOR_DEFAULT_ICON
if self.state in FOLDER_SENSOR_ICONS:
return FOLDER_SENSOR_ICONS[self.state]
return FOLDER_SENSOR_ALERT_ICON
@property
def extra_state_attributes(self):
"""Return the state attributes."""
return self._state
@property
def should_poll(self):
"""Return the polling requirement for this sensor."""
return False
@property
def device_info(self):
"""Return device information."""
return {
"identifiers": {(DOMAIN, self._server_id)},
"name": f"Syncthing ({self._syncthing.url})",
"manufacturer": "Syncthing Team",
"sw_version": self._version,
"entry_type": "service",
}
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):
"""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