__init__.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. """Waste Collection Schedule Component."""
  2. import logging
  3. import site
  4. from pathlib import Path
  5. from random import randrange
  6. import homeassistant.helpers.config_validation as cv
  7. import homeassistant.util.dt as dt_util
  8. import voluptuous as vol
  9. from homeassistant.core import HomeAssistant, callback
  10. from homeassistant.helpers.dispatcher import dispatcher_send
  11. from .const import DOMAIN, UPDATE_SENSORS_SIGNAL
  12. from homeassistant.helpers.event import async_call_later # isort:skip
  13. from homeassistant.helpers.event import async_track_time_change # isort:skip
  14. # add module directory to path
  15. package_dir = Path(__file__).resolve().parents[0]
  16. site.addsitedir(str(package_dir))
  17. from waste_collection_schedule import Customize, Scraper # type: ignore # isort:skip # noqa: E402
  18. _LOGGER = logging.getLogger(__name__)
  19. CONF_SOURCES = "sources"
  20. CONF_SOURCE_NAME = "name"
  21. CONF_SOURCE_ARGS = "args" # scraper-source arguments
  22. CONF_SOURCE_CALENDAR_TITLE = "calendar_title"
  23. CONF_SEPARATOR = "separator"
  24. CONF_FETCH_TIME = "fetch_time"
  25. CONF_RANDOM_FETCH_TIME_OFFSET = "random_fetch_time_offset"
  26. CONF_DAY_SWITCH_TIME = "day_switch_time"
  27. CONF_CUSTOMIZE = "customize"
  28. CONF_TYPE = "type"
  29. CONF_ALIAS = "alias"
  30. CONF_SHOW = "show"
  31. CONF_ICON = "icon"
  32. CONF_PICTURE = "picture"
  33. CONF_USE_DEDICATED_CALENDAR = "use_dedicated_calendar"
  34. CONF_DEDICATED_CALENDAR_TITLE = "dedicated_calendar_title"
  35. CUSTOMIZE_CONFIG = vol.Schema(
  36. {
  37. vol.Optional(CONF_TYPE): cv.string,
  38. vol.Optional(CONF_ALIAS): cv.string,
  39. vol.Optional(CONF_SHOW): cv.boolean,
  40. vol.Optional(CONF_ICON): cv.icon,
  41. vol.Optional(CONF_PICTURE): cv.string,
  42. vol.Optional(CONF_USE_DEDICATED_CALENDAR): cv.boolean,
  43. vol.Optional(CONF_DEDICATED_CALENDAR_TITLE): cv.string,
  44. }
  45. )
  46. SOURCE_CONFIG = vol.Schema(
  47. {
  48. vol.Required(CONF_SOURCE_NAME): cv.string,
  49. vol.Required(CONF_SOURCE_ARGS): dict,
  50. vol.Optional(CONF_CUSTOMIZE, default=[]): vol.All(
  51. cv.ensure_list, [CUSTOMIZE_CONFIG]
  52. ),
  53. vol.Optional(CONF_SOURCE_CALENDAR_TITLE): cv.string,
  54. }
  55. )
  56. CONFIG_SCHEMA = vol.Schema(
  57. {
  58. DOMAIN: vol.Schema(
  59. {
  60. vol.Required(CONF_SOURCES): vol.All(cv.ensure_list, [SOURCE_CONFIG]),
  61. vol.Optional(CONF_SEPARATOR, default=", "): cv.string,
  62. vol.Optional(CONF_FETCH_TIME, default="01:00"): cv.time,
  63. vol.Optional(
  64. CONF_RANDOM_FETCH_TIME_OFFSET, default=60
  65. ): cv.positive_int,
  66. vol.Optional(CONF_DAY_SWITCH_TIME, default="10:00"): cv.time,
  67. }
  68. )
  69. },
  70. extra=vol.ALLOW_EXTRA,
  71. )
  72. async def async_setup(hass: HomeAssistant, config: dict):
  73. """Set up the component. config contains data from configuration.yaml."""
  74. # create empty api object as singleton
  75. api = WasteCollectionApi(
  76. hass,
  77. separator=config[DOMAIN][CONF_SEPARATOR],
  78. fetch_time=config[DOMAIN][CONF_FETCH_TIME],
  79. random_fetch_time_offset=config[DOMAIN][CONF_RANDOM_FETCH_TIME_OFFSET],
  80. day_switch_time=config[DOMAIN][CONF_DAY_SWITCH_TIME],
  81. )
  82. # create scraper(s)
  83. for source in config[DOMAIN][CONF_SOURCES]:
  84. # create customize object
  85. customize = {}
  86. for c in source.get(CONF_CUSTOMIZE, {}):
  87. customize[c[CONF_TYPE]] = Customize(
  88. waste_type=c[CONF_TYPE],
  89. alias=c.get(CONF_ALIAS),
  90. show=c.get(CONF_SHOW, True),
  91. icon=c.get(CONF_ICON),
  92. picture=c.get(CONF_PICTURE),
  93. use_dedicated_calendar=c.get(CONF_USE_DEDICATED_CALENDAR, False),
  94. dedicated_calendar_title=c.get(CONF_DEDICATED_CALENDAR_TITLE, False),
  95. )
  96. api.add_scraper(
  97. source_name=source[CONF_SOURCE_NAME],
  98. customize=customize,
  99. calendar_title=source.get(CONF_SOURCE_CALENDAR_TITLE),
  100. source_args=source.get(CONF_SOURCE_ARGS, {}),
  101. )
  102. # store api object
  103. hass.data.setdefault(DOMAIN, api)
  104. # load calendar platform
  105. await hass.helpers.discovery.async_load_platform(
  106. "calendar", DOMAIN, {"api": api}, config
  107. )
  108. # initial fetch of all data
  109. hass.add_job(api._fetch)
  110. return True
  111. class WasteCollectionApi:
  112. def __init__(
  113. self, hass, separator, fetch_time, random_fetch_time_offset, day_switch_time
  114. ):
  115. self._hass = hass
  116. self._scrapers = []
  117. self._separator = separator
  118. self._fetch_time = fetch_time
  119. self._random_fetch_time_offset = random_fetch_time_offset
  120. self._day_switch_time = day_switch_time
  121. # start timer to fetch date once per day
  122. async_track_time_change(
  123. hass,
  124. self._fetch_callback,
  125. self._fetch_time.hour,
  126. self._fetch_time.minute,
  127. self._fetch_time.second,
  128. )
  129. # start timer for day-switch time
  130. if self._day_switch_time != self._fetch_time:
  131. async_track_time_change(
  132. hass,
  133. self._update_sensors_callback,
  134. self._day_switch_time.hour,
  135. self._day_switch_time.minute,
  136. self._day_switch_time.second,
  137. )
  138. # add a timer at midnight (if not already there) to update days-to
  139. midnight = dt_util.parse_time("00:00")
  140. if midnight != self._fetch_time and midnight != self._day_switch_time:
  141. async_track_time_change(
  142. hass,
  143. self._update_sensors_callback,
  144. midnight.hour,
  145. midnight.minute,
  146. midnight.second,
  147. )
  148. @property
  149. def separator(self):
  150. """Separator string, used to separator waste types."""
  151. return self._separator
  152. @property
  153. def fetch_time(self):
  154. """When to fetch to data."""
  155. return self._fetch_time
  156. @property
  157. def day_switch_time(self):
  158. """When to hide entries for today."""
  159. return self._day_switch_time
  160. def add_scraper(
  161. self,
  162. source_name,
  163. customize,
  164. source_args,
  165. calendar_title,
  166. ):
  167. self._scrapers.append(
  168. Scraper.create(
  169. source_name=source_name,
  170. customize=customize,
  171. source_args=source_args,
  172. calendar_title=calendar_title,
  173. )
  174. )
  175. def _fetch(self, *_):
  176. for scraper in self._scrapers:
  177. scraper.fetch()
  178. self._update_sensors_callback()
  179. @property
  180. def scrapers(self):
  181. return self._scrapers
  182. def get_scraper(self, index):
  183. return self._scrapers[index] if index < len(self._scrapers) else None
  184. @callback
  185. def _fetch_callback(self, *_):
  186. async_call_later(
  187. self._hass,
  188. randrange(0, 60 * self._random_fetch_time_offset),
  189. self._fetch_now_callback,
  190. )
  191. @callback
  192. def _fetch_now_callback(self, *_):
  193. self._hass.add_job(self._fetch)
  194. @callback
  195. def _update_sensors_callback(self, *_):
  196. dispatcher_send(self._hass, UPDATE_SENSORS_SIGNAL)