sensor.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. """Sensor platform support for Waste Collection Schedule."""
  2. import datetime
  3. import logging
  4. from enum import Enum
  5. import homeassistant.helpers.config_validation as cv
  6. import voluptuous as vol
  7. from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
  8. from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE
  9. from homeassistant.core import callback
  10. from homeassistant.helpers.dispatcher import async_dispatcher_connect
  11. from .const import DOMAIN, UPDATE_SENSORS_SIGNAL
  12. _LOGGER = logging.getLogger(__name__)
  13. CONF_SOURCE_INDEX = "source_index"
  14. CONF_DETAILS_FORMAT = "details_format"
  15. CONF_COUNT = "count"
  16. CONF_LEADTIME = "leadtime"
  17. CONF_DATE_TEMPLATE = "date_template"
  18. CONF_COLLECTION_TYPES = "types"
  19. CONF_ADD_DAYS_TO = "add_days_to"
  20. class DetailsFormat(Enum):
  21. """Values for CONF_DETAILS_FORMAT."""
  22. upcoming = "upcoming" # list of "<date> <type1, type2, ...>"
  23. appointment_types = "appointment_types" # list of "<type> <date>"
  24. generic = "generic" # all values in separate attributes
  25. PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
  26. {
  27. vol.Required(CONF_NAME): cv.string,
  28. vol.Optional(CONF_SOURCE_INDEX, default=0): cv.positive_int,
  29. vol.Optional(CONF_DETAILS_FORMAT, default="upcoming"): cv.enum(DetailsFormat),
  30. vol.Optional(CONF_COUNT): cv.positive_int,
  31. vol.Optional(CONF_LEADTIME): cv.positive_int,
  32. vol.Optional(CONF_COLLECTION_TYPES): cv.ensure_list,
  33. vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
  34. vol.Optional(CONF_DATE_TEMPLATE): cv.template,
  35. vol.Optional(CONF_ADD_DAYS_TO, default=False): cv.boolean,
  36. }
  37. )
  38. async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
  39. value_template = config.get(CONF_VALUE_TEMPLATE)
  40. if value_template is not None:
  41. value_template.hass = hass
  42. date_template = config.get(CONF_DATE_TEMPLATE)
  43. if date_template is not None:
  44. date_template.hass = hass
  45. entities = []
  46. entities.append(
  47. ScheduleSensor(
  48. hass=hass,
  49. api=hass.data[DOMAIN],
  50. name=config[CONF_NAME],
  51. source_index=config[CONF_SOURCE_INDEX],
  52. details_format=config[CONF_DETAILS_FORMAT],
  53. count=config.get(CONF_COUNT),
  54. leadtime=config.get(CONF_LEADTIME),
  55. collection_types=config.get(CONF_COLLECTION_TYPES),
  56. value_template=value_template,
  57. date_template=date_template,
  58. add_days_to=config.get(CONF_ADD_DAYS_TO),
  59. )
  60. )
  61. async_add_entities(entities)
  62. class ScheduleSensor(SensorEntity):
  63. """Base for sensors."""
  64. def __init__(
  65. self,
  66. hass,
  67. api,
  68. name,
  69. source_index,
  70. details_format,
  71. count,
  72. leadtime,
  73. collection_types,
  74. value_template,
  75. date_template,
  76. add_days_to,
  77. ):
  78. """Initialize the entity."""
  79. self._api = api
  80. self._source_index = source_index
  81. self._details_format = details_format
  82. self._count = count
  83. self._leadtime = leadtime
  84. self._collection_types = collection_types
  85. self._value_template = value_template
  86. self._date_template = date_template
  87. self._add_days_to = add_days_to
  88. self._value = None
  89. # entity attributes
  90. self._attr_name = name
  91. self._attr_unique_id = name
  92. self._attr_should_poll = False
  93. async_dispatcher_connect(hass, UPDATE_SENSORS_SIGNAL, self._update_sensor)
  94. @property
  95. def native_value(self):
  96. """Return the state of the entity."""
  97. return self._value
  98. async def async_added_to_hass(self):
  99. """Entities have been added to hass."""
  100. self._update_sensor()
  101. @property
  102. def _scraper(self):
  103. return self._api.get_scraper(self._source_index)
  104. @property
  105. def _separator(self):
  106. """Return separator string used to join waste types."""
  107. return self._api.separator
  108. @property
  109. def _include_today(self):
  110. """Return true if collections for today shall be included in the results."""
  111. return datetime.datetime.now().time() < self._api._day_switch_time
  112. def _add_refreshtime(self):
  113. """Add refresh-time (= last fetch time) to device-state-attributes."""
  114. refreshtime = ""
  115. if self._scraper.refreshtime is not None:
  116. refreshtime = self._scraper.refreshtime.strftime("%x %X")
  117. self._attr_attribution = f"Last update: {refreshtime}"
  118. def _set_state(self, upcoming):
  119. """Set entity state with default format."""
  120. if len(upcoming) == 0:
  121. self._value = None
  122. self._attr_icon = "mdi:trash-can"
  123. self._attr_entity_picture = None
  124. return
  125. collection = upcoming[0]
  126. # collection::=CollectionGroup{date=2020-04-01, types=['Type1', 'Type2']}
  127. if self._value_template is not None:
  128. self._value = self._value_template.async_render_with_possible_json_value(
  129. collection, None
  130. )
  131. else:
  132. self._value = (
  133. f"{self._separator.join(collection.types)} in {collection.daysTo} days"
  134. )
  135. self._attr_icon = collection.icon or "mdi:trash-can"
  136. self._attr_entity_picture = collection.picture
  137. def _render_date(self, collection):
  138. if self._date_template is not None:
  139. return self._date_template.async_render_with_possible_json_value(
  140. collection, None
  141. )
  142. else:
  143. return collection.date.isoformat()
  144. @callback
  145. def _update_sensor(self):
  146. """Update the state and the device-state-attributes of the entity.
  147. Called if a new data has been fetched from the scraper source.
  148. """
  149. if self._scraper is None:
  150. _LOGGER.error(f"source_index {self._source_index} out of range")
  151. return None
  152. upcoming1 = self._scraper.get_upcoming_group_by_day(
  153. count=1, types=self._collection_types, include_today=self._include_today,
  154. )
  155. self._set_state(upcoming1)
  156. attributes = {}
  157. collection_types = (
  158. sorted(self._scraper.get_types())
  159. if self._collection_types is None
  160. else self._collection_types
  161. )
  162. if self._details_format == DetailsFormat.upcoming:
  163. # show upcoming events list in details
  164. upcoming = self._scraper.get_upcoming_group_by_day(
  165. count=self._count,
  166. leadtime=self._leadtime,
  167. types=self._collection_types,
  168. include_today=self._include_today,
  169. )
  170. for collection in upcoming:
  171. attributes[self._render_date(collection)] = self._separator.join(
  172. collection.types
  173. )
  174. elif self._details_format == DetailsFormat.appointment_types:
  175. # show list of collections in details
  176. for t in collection_types:
  177. collections = self._scraper.get_upcoming(
  178. count=1, types=[t], include_today=self._include_today
  179. )
  180. date = (
  181. "" if len(collections) == 0 else self._render_date(collections[0])
  182. )
  183. attributes[t] = date
  184. elif self._details_format == DetailsFormat.generic:
  185. # insert generic attributes into details
  186. attributes["types"] = collection_types
  187. attributes["upcoming"] = self._scraper.get_upcoming(
  188. count=self._count,
  189. leadtime=self._leadtime,
  190. types=self._collection_types,
  191. include_today=self._include_today,
  192. )
  193. refreshtime = ""
  194. if self._scraper.refreshtime is not None:
  195. refreshtime = self._scraper.refreshtime.isoformat(timespec="seconds")
  196. attributes["last_update"] = refreshtime
  197. if len(upcoming1) > 0:
  198. if self._add_days_to:
  199. attributes["daysTo"] = upcoming1[0].daysTo
  200. self._attr_extra_state_attributes = attributes
  201. self._add_refreshtime()
  202. if self.hass is not None:
  203. self.async_write_ha_state()