Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for monitoring the Syncthing instance."""
2 
3 import aiosyncthing
4 
5 from homeassistant.components.sensor import SensorEntity
6 from homeassistant.config_entries import ConfigEntry
7 from homeassistant.core import HomeAssistant, callback
8 from homeassistant.exceptions import PlatformNotReady
9 from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
10 from homeassistant.helpers.dispatcher import async_dispatcher_connect
11 from homeassistant.helpers.entity_platform import AddEntitiesCallback
12 from homeassistant.helpers.event import async_track_time_interval
13 
14 from .const import (
15  DOMAIN,
16  FOLDER_PAUSED_RECEIVED,
17  FOLDER_SUMMARY_RECEIVED,
18  SCAN_INTERVAL,
19  SERVER_AVAILABLE,
20  SERVER_UNAVAILABLE,
21  STATE_CHANGED_RECEIVED,
22 )
23 
24 
26  hass: HomeAssistant,
27  config_entry: ConfigEntry,
28  async_add_entities: AddEntitiesCallback,
29 ) -> None:
30  """Set up the Syncthing sensors."""
31  syncthing = hass.data[DOMAIN][config_entry.entry_id]
32 
33  try:
34  config = await syncthing.system.config()
35  version = await syncthing.system.version()
36  except aiosyncthing.exceptions.SyncthingError as exception:
37  raise PlatformNotReady from exception
38 
39  server_id = syncthing.server_id
40  entities = [
42  syncthing,
43  server_id,
44  folder["id"],
45  folder["label"],
46  version["version"],
47  )
48  for folder in config["folders"]
49  ]
50 
51  async_add_entities(entities)
52 
53 
55  """A Syncthing folder sensor."""
56 
57  _attr_should_poll = False
58  _attr_translation_key = "syncthing"
59 
60  STATE_ATTRIBUTES = {
61  "errors": "errors",
62  "globalBytes": "global_bytes",
63  "globalDeleted": "global_deleted",
64  "globalDirectories": "global_directories",
65  "globalFiles": "global_files",
66  "globalSymlinks": "global_symlinks",
67  "globalTotalItems": "global_total_items",
68  "ignorePatterns": "ignore_patterns",
69  "inSyncBytes": "in_sync_bytes",
70  "inSyncFiles": "in_sync_files",
71  "invalid": "invalid",
72  "localBytes": "local_bytes",
73  "localDeleted": "local_deleted",
74  "localDirectories": "local_directories",
75  "localFiles": "local_files",
76  "localSymlinks": "local_symlinks",
77  "localTotalItems": "local_total_items",
78  "needBytes": "need_bytes",
79  "needDeletes": "need_deletes",
80  "needDirectories": "need_directories",
81  "needFiles": "need_files",
82  "needSymlinks": "need_symlinks",
83  "needTotalItems": "need_total_items",
84  "pullErrors": "pull_errors",
85  "state": "state",
86  }
87 
88  def __init__(self, syncthing, server_id, folder_id, folder_label, version):
89  """Initialize the sensor."""
90  self._syncthing_syncthing = syncthing
91  self._server_id_server_id = server_id
92  self._folder_id_folder_id = folder_id
93  self._folder_label_folder_label = folder_label
94  self._state_state = None
95  self._unsub_timer_unsub_timer = None
96 
97  self._short_server_id_short_server_id = server_id.split("-")[0]
98  self._attr_name_attr_name = f"{self._short_server_id} {folder_id} {folder_label}"
99  self._attr_unique_id_attr_unique_id = f"{self._short_server_id}-{folder_id}"
100  self._attr_device_info_attr_device_info = DeviceInfo(
101  entry_type=DeviceEntryType.SERVICE,
102  identifiers={(DOMAIN, self._server_id_server_id)},
103  manufacturer="Syncthing Team",
104  name=f"Syncthing ({syncthing.url})",
105  sw_version=version,
106  )
107 
108  @property
109  def native_value(self):
110  """Return the state of the sensor."""
111  return self._state_state["state"]
112 
113  @property
114  def available(self):
115  """Could the device be accessed during the last update call."""
116  return self._state_state is not None
117 
118  @property
120  """Return the state attributes."""
121  return self._state_state
122 
123  async def async_update_status(self):
124  """Request folder status and update state."""
125  try:
126  state = await self._syncthing_syncthing.database.status(self._folder_id_folder_id)
127  except aiosyncthing.exceptions.SyncthingError:
128  self._state_state = None
129  else:
130  self._state_state = self._filter_state_filter_state(state)
131  self.async_write_ha_stateasync_write_ha_state()
132 
133  def subscribe(self):
134  """Start polling syncthing folder status."""
135  if self._unsub_timer_unsub_timer is None:
136 
137  async def refresh(event_time):
138  """Get the latest data from Syncthing."""
139  await self.async_update_statusasync_update_status()
140 
141  self._unsub_timer_unsub_timer = async_track_time_interval(
142  self.hasshass, refresh, SCAN_INTERVAL
143  )
144 
145  @callback
146  def unsubscribe(self):
147  """Stop polling syncthing folder status."""
148  if self._unsub_timer_unsub_timer is not None:
149  self._unsub_timer_unsub_timer()
150  self._unsub_timer_unsub_timer = None
151 
152  async def async_added_to_hass(self) -> None:
153  """Handle entity which will be added."""
154 
155  @callback
156  def handle_folder_summary(event):
157  if self._state_state is not None:
158  self._state_state = self._filter_state_filter_state(event["data"]["summary"])
159  self.async_write_ha_stateasync_write_ha_state()
160 
161  self.async_on_removeasync_on_remove(
163  self.hasshass,
164  f"{FOLDER_SUMMARY_RECEIVED}-{self._server_id}-{self._folder_id}",
165  handle_folder_summary,
166  )
167  )
168 
169  @callback
170  def handle_state_changed(event):
171  if self._state_state is not None:
172  self._state_state["state"] = event["data"]["to"]
173  self.async_write_ha_stateasync_write_ha_state()
174 
175  self.async_on_removeasync_on_remove(
177  self.hasshass,
178  f"{STATE_CHANGED_RECEIVED}-{self._server_id}-{self._folder_id}",
179  handle_state_changed,
180  )
181  )
182 
183  @callback
184  def handle_folder_paused(event):
185  if self._state_state is not None:
186  self._state_state["state"] = "paused"
187  self.async_write_ha_stateasync_write_ha_state()
188 
189  self.async_on_removeasync_on_remove(
191  self.hasshass,
192  f"{FOLDER_PAUSED_RECEIVED}-{self._server_id}-{self._folder_id}",
193  handle_folder_paused,
194  )
195  )
196 
197  @callback
198  def handle_server_unavailable():
199  self._state_state = None
200  self.unsubscribeunsubscribe()
201  self.async_write_ha_stateasync_write_ha_state()
202 
203  self.async_on_removeasync_on_remove(
205  self.hasshass,
206  f"{SERVER_UNAVAILABLE}-{self._server_id}",
207  handle_server_unavailable,
208  )
209  )
210 
211  async def handle_server_available():
212  self.subscribesubscribe()
213  await self.async_update_statusasync_update_status()
214 
215  self.async_on_removeasync_on_remove(
217  self.hasshass,
218  f"{SERVER_AVAILABLE}-{self._server_id}",
219  handle_server_available,
220  )
221  )
222 
223  self.subscribesubscribe()
224  self.async_on_removeasync_on_remove(self.unsubscribeunsubscribe)
225 
226  await self.async_update_statusasync_update_status()
227 
228  def _filter_state(self, state):
229  # Select only needed state attributes and map their names
230  state = {
231  self.STATE_ATTRIBUTESSTATE_ATTRIBUTES[key]: value
232  for key, value in state.items()
233  if key in self.STATE_ATTRIBUTESSTATE_ATTRIBUTES
234  }
235 
236  # A workaround, for some reason, state of paused folders is an empty string
237  if state["state"] == "":
238  state["state"] = "paused"
239 
240  # Add some useful attributes
241  state["id"] = self._folder_id_folder_id
242  state["label"] = self._folder_label_folder_label
243 
244  return state
def __init__(self, syncthing, server_id, folder_id, folder_label, version)
Definition: sensor.py:88
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:29
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103
CALLBACK_TYPE async_track_time_interval(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, timedelta interval, *str|None name=None, bool|None cancel_on_shutdown=None)
Definition: event.py:1679