Add syncthing integration (#38331)
* Scaffold the integration * Add config flow data schema * Handle configuration errors * Get folder states * Support https * Fix translations * Listen to syncthing events in a separate thread * Bump syncthing * Automatically reconnect to the syncthing server * Renames * Improve loading and unloading * Update folder states from events * Refactoring, handle FolderPaused event * Dynamic folder icons * Refactoring * Mark folders as unavailable when senrver is unavailable * Update folder satus when server is available * Raise PlatformNotReady * Implement additional polling * Stop polling when the server is not available * Minor fixes * Remove logging * Check name uniqueness * Refactoring * Minor refactorings * Bump python-syncthing * Migrate to aiosyncthing * Minor fixes * Update .coveragerc * Set quality scale * Bump aiosyncthing, properly handle invalid token * Fix logging * Fix logging * Use CONF_VERIFY_SSL from homeassistant.const * Bump aiosyncthing. Add Syncthing device * Fix device name * Bump aiosyncthing * Bump aiosyncthing * Extract SyncthingClient * Add folder to device_state_attributes * Do not pass the loop * Cover config_flow.py * Move self.async_create_entry outside of the try block * Raise ConfigEntryNotReady if syncthing server is not reachable * Fix already configured error message * Change default name to Syncthing * Bump aiosyncthing * Fix formatting * Fix formatting * Fix tests * Fix typo, use lis comprehension * Fix typo, remove unused CONFIG_SCHEMA * Bump aiosyncthing * Remove periods from log messages W0001 * Fix tests * Black, isort * Remove empty items from manifest.json * Fix variable naming * Remove async_setup * Use SensorEntity * Use asyncio.create_task instead of self._hass.loop.create_task * Do not pass hass to FolderSensor initializer * Rename device_state_attributes to extra_state_attributes * Use callbacks * Simplify tests * Refactor _listen() * Use url for the title * Use the url instead of the name to identify the config entry * Explicitly set sensor attributes, extract _filter_state * Use server url instead of name in device_info * Use server url instead of name in logs * User server id as a device identifier * Use URL instead of name to identify config entry * Use shortened server id instead of name to build entity name and unique id * Do not use CONF_NAME * Cleanup unused strings * Cleanup unused strings * Add IOT class * Scaffold the integration * Add config flow data schema * Handle configuration errors * Get folder states * Support https * Fix translations * Listen to syncthing events in a separate thread * Bump syncthing * Automatically reconnect to the syncthing server * Renames * Improve loading and unloading * Update folder states from events * Refactoring, handle FolderPaused event * Dynamic folder icons * Refactoring * Mark folders as unavailable when senrver is unavailable * Update folder satus when server is available * Raise PlatformNotReady * Implement additional polling * Stop polling when the server is not available * Minor fixes * Remove logging * Check name uniqueness * Refactoring * Minor refactorings * Bump python-syncthing * Migrate to aiosyncthing * Minor fixes * Update .coveragerc * Set quality scale * Bump aiosyncthing, properly handle invalid token * Fix logging * Fix logging * Use CONF_VERIFY_SSL from homeassistant.const * Bump aiosyncthing. Add Syncthing device * Fix device name * Bump aiosyncthing * Bump aiosyncthing * Extract SyncthingClient * Add folder to device_state_attributes * Do not pass the loop * Cover config_flow.py * Move self.async_create_entry outside of the try block * Raise ConfigEntryNotReady if syncthing server is not reachable * Fix already configured error message * Change default name to Syncthing * Bump aiosyncthing * Fix formatting * Fix formatting * Fix tests * Fix typo, use lis comprehension * Fix typo, remove unused CONFIG_SCHEMA * Bump aiosyncthing * Remove periods from log messages W0001 * Fix tests * Black, isort * Remove empty items from manifest.json * Fix variable naming * Remove async_setup * Use SensorEntity * Use asyncio.create_task instead of self._hass.loop.create_task * Do not pass hass to FolderSensor initializer * Rename device_state_attributes to extra_state_attributes * Use callbacks * Simplify tests * Refactor _listen() * Use url for the title * Use the url instead of the name to identify the config entry * Explicitly set sensor attributes, extract _filter_state * Use server url instead of name in device_info * Use server url instead of name in logs * User server id as a device identifier * Use URL instead of name to identify config entry * Use shortened server id instead of name to build entity name and unique id * Do not use CONF_NAME * Cleanup unused strings * Cleanup unused strings * Add IOT class * Apply suggestions from code review * Clean up * Fix dict comprehension * Clean sensor * Use the server ID as a config entry unique ID * Remove the AlreadyConfigured exception * Clean up old error string * Format json * Convert sensor attributes to snake case * Force CI Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
7374b844d7
commit
97eb4c6c62
14 changed files with 718 additions and 0 deletions
268
homeassistant/components/syncthing/sensor.py
Normal file
268
homeassistant/components/syncthing/sensor.py
Normal file
|
@ -0,0 +1,268 @@
|
|||
"""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
|
Loading…
Add table
Add a link
Reference in a new issue