| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- """Sensor platform support for Waste Collection Schedule."""
- import datetime
- import logging
- from enum import Enum
- import homeassistant.helpers.config_validation as cv
- import voluptuous as vol
- from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
- from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE
- from homeassistant.core import callback
- from homeassistant.helpers.dispatcher import async_dispatcher_connect
- from .const import DOMAIN, UPDATE_SENSORS_SIGNAL
- _LOGGER = logging.getLogger(__name__)
- CONF_SOURCE_INDEX = "source_index"
- CONF_DETAILS_FORMAT = "details_format"
- CONF_COUNT = "count"
- CONF_LEADTIME = "leadtime"
- CONF_DATE_TEMPLATE = "date_template"
- CONF_COLLECTION_TYPES = "types"
- CONF_ADD_DAYS_TO = "add_days_to"
- class DetailsFormat(Enum):
- """Values for CONF_DETAILS_FORMAT."""
- upcoming = "upcoming" # list of "<date> <type1, type2, ...>"
- appointment_types = "appointment_types" # list of "<type> <date>"
- generic = "generic" # all values in separate attributes
- PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_NAME): cv.string,
- vol.Optional(CONF_SOURCE_INDEX, default=0): cv.positive_int,
- vol.Optional(CONF_DETAILS_FORMAT, default="upcoming"): cv.enum(DetailsFormat),
- vol.Optional(CONF_COUNT): cv.positive_int,
- vol.Optional(CONF_LEADTIME): cv.positive_int,
- vol.Optional(CONF_COLLECTION_TYPES): cv.ensure_list,
- vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
- vol.Optional(CONF_DATE_TEMPLATE): cv.template,
- vol.Optional(CONF_ADD_DAYS_TO, default=False): cv.boolean,
- }
- )
- async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- value_template = config.get(CONF_VALUE_TEMPLATE)
- if value_template is not None:
- value_template.hass = hass
- date_template = config.get(CONF_DATE_TEMPLATE)
- if date_template is not None:
- date_template.hass = hass
- entities = []
- entities.append(
- ScheduleSensor(
- hass=hass,
- api=hass.data[DOMAIN],
- name=config[CONF_NAME],
- source_index=config[CONF_SOURCE_INDEX],
- details_format=config[CONF_DETAILS_FORMAT],
- count=config.get(CONF_COUNT),
- leadtime=config.get(CONF_LEADTIME),
- collection_types=config.get(CONF_COLLECTION_TYPES),
- value_template=value_template,
- date_template=date_template,
- add_days_to=config.get(CONF_ADD_DAYS_TO),
- )
- )
- async_add_entities(entities)
- class ScheduleSensor(SensorEntity):
- """Base for sensors."""
- def __init__(
- self,
- hass,
- api,
- name,
- source_index,
- details_format,
- count,
- leadtime,
- collection_types,
- value_template,
- date_template,
- add_days_to,
- ):
- """Initialize the entity."""
- self._api = api
- self._source_index = source_index
- self._details_format = details_format
- self._count = count
- self._leadtime = leadtime
- self._collection_types = collection_types
- self._value_template = value_template
- self._date_template = date_template
- self._add_days_to = add_days_to
- self._value = None
- # entity attributes
- self._attr_name = name
- self._attr_unique_id = name
- self._attr_should_poll = False
- async_dispatcher_connect(hass, UPDATE_SENSORS_SIGNAL, self._update_sensor)
- @property
- def native_value(self):
- """Return the state of the entity."""
- return self._value
- async def async_added_to_hass(self):
- """Entities have been added to hass."""
- self._update_sensor()
- @property
- def _scraper(self):
- return self._api.get_scraper(self._source_index)
- @property
- def _separator(self):
- """Return separator string used to join waste types."""
- return self._api.separator
- @property
- def _include_today(self):
- """Return true if collections for today shall be included in the results."""
- return datetime.datetime.now().time() < self._api._day_switch_time
- def _add_refreshtime(self):
- """Add refresh-time (= last fetch time) to device-state-attributes."""
- refreshtime = ""
- if self._scraper.refreshtime is not None:
- refreshtime = self._scraper.refreshtime.strftime("%x %X")
- self._attr_attribution = f"Last update: {refreshtime}"
- def _set_state(self, upcoming):
- """Set entity state with default format."""
- if len(upcoming) == 0:
- self._value = None
- self._attr_icon = "mdi:trash-can"
- self._attr_entity_picture = None
- return
- collection = upcoming[0]
- # collection::=CollectionGroup{date=2020-04-01, types=['Type1', 'Type2']}
- if self._value_template is not None:
- self._value = self._value_template.async_render_with_possible_json_value(
- collection, None
- )
- else:
- self._value = (
- f"{self._separator.join(collection.types)} in {collection.daysTo} days"
- )
- self._attr_icon = collection.icon or "mdi:trash-can"
- self._attr_entity_picture = collection.picture
- def _render_date(self, collection):
- if self._date_template is not None:
- return self._date_template.async_render_with_possible_json_value(
- collection, None
- )
- else:
- return collection.date.isoformat()
- @callback
- def _update_sensor(self):
- """Update the state and the device-state-attributes of the entity.
- Called if a new data has been fetched from the scraper source.
- """
- if self._scraper is None:
- _LOGGER.error(f"source_index {self._source_index} out of range")
- return None
- upcoming1 = self._scraper.get_upcoming_group_by_day(
- count=1, types=self._collection_types, include_today=self._include_today,
- )
- self._set_state(upcoming1)
- attributes = {}
- collection_types = (
- sorted(self._scraper.get_types())
- if self._collection_types is None
- else self._collection_types
- )
- if self._details_format == DetailsFormat.upcoming:
- # show upcoming events list in details
- upcoming = self._scraper.get_upcoming_group_by_day(
- count=self._count,
- leadtime=self._leadtime,
- types=self._collection_types,
- include_today=self._include_today,
- )
- for collection in upcoming:
- attributes[self._render_date(collection)] = self._separator.join(
- collection.types
- )
- elif self._details_format == DetailsFormat.appointment_types:
- # show list of collections in details
- for t in collection_types:
- collections = self._scraper.get_upcoming(
- count=1, types=[t], include_today=self._include_today
- )
- date = (
- "" if len(collections) == 0 else self._render_date(collections[0])
- )
- attributes[t] = date
- elif self._details_format == DetailsFormat.generic:
- # insert generic attributes into details
- attributes["types"] = collection_types
- attributes["upcoming"] = self._scraper.get_upcoming(
- count=self._count,
- leadtime=self._leadtime,
- types=self._collection_types,
- include_today=self._include_today,
- )
- refreshtime = ""
- if self._scraper.refreshtime is not None:
- refreshtime = self._scraper.refreshtime.isoformat(timespec="seconds")
- attributes["last_update"] = refreshtime
- if len(upcoming1) > 0:
- if self._add_days_to:
- attributes["daysTo"] = upcoming1[0].daysTo
- self._attr_extra_state_attributes = attributes
- self._add_refreshtime()
- if self.hass is not None:
- self.async_write_ha_state()
|