async def async_initialize(self): """Perform async initialization of config.""" await super().async_initialize() async def hass_started(hass): if self.enabled and GOOGLE_DOMAIN not in self.hass.config.components: await async_setup_component(self.hass, GOOGLE_DOMAIN, {}) start.async_at_start(self.hass, hass_started) # Remove any stored user agent id that is not ours remove_agent_user_ids = [] for agent_user_id in self._store.agent_user_ids: if agent_user_id != self.agent_user_id: remove_agent_user_ids.append(agent_user_id) for agent_user_id in remove_agent_user_ids: await self.async_disconnect_agent_user(agent_user_id) self._prefs.async_listen_updates(self._async_prefs_updated) self.hass.bus.async_listen( er.EVENT_ENTITY_REGISTRY_UPDATED, self._handle_entity_registry_updated, ) self.hass.bus.async_listen( dr.EVENT_DEVICE_REGISTRY_UPDATED, self._handle_device_registry_updated, )
async def async_get_instance(hass: HomeAssistant) -> RestoreStateData: """Get the singleton instance of this data helper.""" data = RestoreStateData(hass) try: stored_states = await data.store.async_load() except HomeAssistantError as exc: _LOGGER.error("Error loading last states", exc_info=exc) stored_states = None if stored_states is None: _LOGGER.debug("Not creating cache - no saved states found") data.last_states = {} else: data.last_states = { item["state"]["entity_id"]: StoredState.from_dict(item) for item in stored_states if valid_entity_id(item["state"]["entity_id"]) } _LOGGER.debug("Created cache with %s", list(data.last_states)) async def hass_start(hass: HomeAssistant) -> None: """Start the restore state task.""" data.async_setup_dump() start.async_at_start(hass, hass_start) return data
async def async_added_to_hass(self) -> None: """Register listeners.""" async def _update_at_start(_): self.async_update_group_state() self.async_write_ha_state() start.async_at_start(self.hass, _update_at_start)
async def async_initialize(self): """Initialize the Alexa config.""" async def hass_started(hass): if self.enabled and ALEXA_DOMAIN not in self.hass.config.components: await async_setup_component(self.hass, ALEXA_DOMAIN, {}) start.async_at_start(self.hass, hass_started)
async def test_at_start_when_running(hass): """Test at start when already running.""" assert hass.is_running calls = [] async def cb_at_start(hass): """Home Assistant is started.""" calls.append(1) start.async_at_start(hass, cb_at_start) await hass.async_block_till_done() assert len(calls) == 1
async def async_initialize(self): """Perform async initialization of config.""" self._store = GoogleConfigStore(self.hass) await self._store.async_load() if not self.enabled: return async def sync_google(_): """Sync entities to Google.""" await self.async_sync_entities_all() start.async_at_start(self.hass, sync_google)
async def async_initialize(self): """Initialize the Alexa config.""" async def hass_started(hass): if self.enabled and ALEXA_DOMAIN not in self.hass.config.components: await async_setup_component(self.hass, ALEXA_DOMAIN, {}) start.async_at_start(self.hass, hass_started) self._prefs.async_listen_updates(self._async_prefs_updated) self.hass.bus.async_listen( entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, self._handle_entity_registry_updated, )
async def test_at_start_when_starting_callback(hass, caplog): """Test at start when yet to start.""" hass.state = core.CoreState.not_running assert not hass.is_running calls = [] @core.callback def cb_at_start(hass): """Home Assistant is started.""" calls.append(1) cancel = start.async_at_start(hass, cb_at_start) await hass.async_block_till_done() assert len(calls) == 0 hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() assert len(calls) == 1 cancel() # Check the unnecessary cancel did not generate warnings or errors for record in caplog.records: assert record.levelname in ("DEBUG", "INFO")
async def async_added_to_hass(self) -> None: """Wait for start so origin and destination entities can be resolved.""" await super().async_added_to_hass() async def _update_at_start(_): await self.async_update() self.async_on_remove(async_at_start(self.hass, _update_at_start))
async def test_at_start_when_starting_awaitable(hass): """Test at start when yet to start.""" hass.state = core.CoreState.not_running assert not hass.is_running calls = [] async def cb_at_start(hass): """Home Assistant is started.""" calls.append(1) start.async_at_start(hass, cb_at_start) await hass.async_block_till_done() assert len(calls) == 0 hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() assert len(calls) == 1
async def test_cancelling_when_running(hass, caplog): """Test cancelling at start when already running.""" assert hass.state == core.CoreState.running assert hass.is_running calls = [] async def cb_at_start(hass): """Home Assistant is started.""" calls.append(1) start.async_at_start(hass, cb_at_start)() await hass.async_block_till_done() assert len(calls) == 1 # Check the unnecessary cancel did not generate warnings or errors for record in caplog.records: assert record.levelname in ("DEBUG", "INFO")
async def test_at_start_when_running_callback(hass): """Test at start when already running.""" assert hass.state == core.CoreState.running assert hass.is_running calls = [] @core.callback def cb_at_start(hass): """Home Assistant is started.""" calls.append(1) start.async_at_start(hass, cb_at_start) assert len(calls) == 1 hass.state = core.CoreState.starting assert hass.is_running start.async_at_start(hass, cb_at_start) assert len(calls) == 2
async def async_startup(): """HACS startup tasks.""" hacs.enable_hacs() for location in ( hass.config.path("custom_components/custom_updater.py"), hass.config.path("custom_components/custom_updater/__init__.py"), ): if os.path.exists(location): hacs.log.critical( "This cannot be used with custom_updater. " "To use this you need to remove custom_updater form %s", location, ) hacs.disable_hacs(HacsDisabledReason.CONSTRAINS) return False if not version_left_higher_or_equal_then_right( hacs.core.ha_version.string, MINIMUM_HA_VERSION, ): hacs.log.critical( "You need HA version %s or newer to use this integration.", MINIMUM_HA_VERSION, ) hacs.disable_hacs(HacsDisabledReason.CONSTRAINS) return False if not await hacs.data.restore(): hacs.disable_hacs(HacsDisabledReason.RESTORE) return False can_update = await hacs.async_can_update() hacs.log.debug("Can update %s repositories", can_update) hacs.set_active_categories() async_register_websocket_commands(hass) async_register_frontend(hass, hacs) if hacs.configuration.config_type == ConfigurationType.YAML: hass.async_create_task( async_load_platform(hass, Platform.SENSOR, DOMAIN, {}, hacs.configuration.config) ) hacs.log.info("Update entities are only supported when using UI configuration") else: if hacs.configuration.experimental: hass.config_entries.async_setup_platforms( hacs.configuration.config_entry, [Platform.SENSOR, Platform.UPDATE] ) else: hass.config_entries.async_setup_platforms( hacs.configuration.config_entry, [Platform.SENSOR] ) hacs.set_stage(HacsStage.SETUP) if hacs.system.disabled: return False # Schedule startup tasks async_at_start(hass=hass, at_start_cb=hacs.startup_tasks) hacs.set_stage(HacsStage.WAITING) hacs.log.info("Setup complete, waiting for Home Assistant before startup tasks starts") return not hacs.system.disabled
def _async_add_listener(self) -> None: """Add a listener to start tracking state changes after start.""" self._at_start_listener = async_at_start( self.hass, self._async_add_events_listener)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up from a config entry.""" http_session = async_get_clientsession(hass, verify_ssl=False) db_file = hass.config.path("music_assistant.db") # databases is really chatty with logging at info level logging.getLogger("databases").setLevel(logging.WARNING) conf = entry.options # TODO: adjust config flow to support creating multiple provider entries providers = [] if conf.get(CONF_SPOTIFY_ENABLED): providers.append( MusicProviderConfig( ProviderType.SPOTIFY, username=conf.get(CONF_SPOTIFY_USERNAME), password=conf.get(CONF_SPOTIFY_PASSWORD), )) if conf.get(CONF_QOBUZ_ENABLED): providers.append( MusicProviderConfig( ProviderType.QOBUZ, username=conf.get(CONF_QOBUZ_USERNAME), password=conf.get(CONF_QOBUZ_PASSWORD), )) if conf.get(CONF_TUNEIN_ENABLED): providers.append( MusicProviderConfig( ProviderType.TUNEIN, username=conf.get(CONF_TUNEIN_USERNAME), )) if conf.get(CONF_FILE_ENABLED) and conf.get(CONF_FILE_DIRECTORY): providers.append( MusicProviderConfig( ProviderType.FILESYSTEM_LOCAL, path=conf.get(CONF_FILE_DIRECTORY), )) stream_ip = get_local_ip_from_internal_url(hass) mass_conf = MassConfig(database_url=f"sqlite:///{db_file}", providers=providers, stream_ip=stream_ip) mass = MusicAssistant(mass_conf, session=http_session) try: await mass.setup() except MusicAssistantError as err: await mass.stop() LOGGER.exception(err) raise ConfigEntryNotReady from err except Exception as exc: # pylint: disable=broad-except await mass.stop() raise exc hass.data[DOMAIN] = mass # initialize platforms if conf.get(CONF_CREATE_MASS_PLAYERS, True): hass.config_entries.async_setup_platforms(entry, PLATFORMS) async def on_hass_start(*args, **kwargs): """Start sync actions when Home Assistant is started.""" register_services(hass, mass) # register hass players with mass await async_register_player_controls(hass, mass, entry) # start and schedule sync (every 3 hours) await mass.music.start_sync(schedule=3) async def on_hass_stop(event: Event): """Handle an incoming stop event from Home Assistant.""" await mass.stop() async def on_mass_event(event: MassEvent): """Handle an incoming event from Music Assistant.""" # forward event to the HA eventbus if hasattr(event.data, "to_dict"): data = event.data.to_dict() else: data = event.data hass.bus.async_fire( DOMAIN_EVENT, { "type": event.type.value, "object_id": event.object_id, "data": data }, ) # setup event listeners, register their unsubscribe in the unload entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)) entry.async_on_unload(async_at_start(hass, on_hass_start)) entry.async_on_unload(entry.add_update_listener(_update_listener)) entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)) entry.async_on_unload(entry.add_update_listener(_update_listener)) entry.async_on_unload(mass.subscribe(on_mass_event, FORWARD_EVENTS)) # Websocket support and frontend (panel) async_register_websockets(hass) entry.async_on_unload(await async_register_panel(hass, entry.title)) # cleanup orphan devices/entities dev_reg = dr.async_get(hass) stored_devices = dr.async_entries_for_config_entry(dev_reg, entry.entry_id) if CONF_PLAYER_ENTITIES in entry.options: for device in stored_devices: for _, player_id in device.identifiers: if player_id not in entry.options[CONF_PLAYER_ENTITIES]: dev_reg.async_remove_device(device.id) elif not entry.options[CONF_CREATE_MASS_PLAYERS]: dev_reg.async_remove_device(device.id) return True
async def async_setup_platform( hass: HomeAssistant, config: ConfigType, async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the SAJ sensors.""" remove_interval_update = None wifi = config[CONF_TYPE] == INVERTER_TYPES[1] # Init all sensors sensor_def = pysaj.Sensors(wifi) # Use all sensors by default hass_sensors = [] kwargs = {} if wifi: kwargs["wifi"] = True if config.get(CONF_USERNAME) and config.get(CONF_PASSWORD): kwargs["username"] = config[CONF_USERNAME] kwargs["password"] = config[CONF_PASSWORD] try: saj = pysaj.SAJ(config[CONF_HOST], **kwargs) done = await saj.read(sensor_def) except pysaj.UnauthorizedException: _LOGGER.error("Username and/or password is wrong") return except pysaj.UnexpectedResponseException as err: _LOGGER.error( "Error in SAJ, please check host/ip address. Original error: %s", err) return if not done: raise PlatformNotReady for sensor in sensor_def: if sensor.enabled: hass_sensors.append( SAJsensor(saj.serialnumber, sensor, inverter_name=config.get(CONF_NAME))) async_add_entities(hass_sensors) async def async_saj(): """Update all the SAJ sensors.""" values = await saj.read(sensor_def) for sensor in hass_sensors: state_unknown = False # SAJ inverters are powered by DC via solar panels and thus are # offline after the sun has set. If a sensor resets on a daily # basis like "today_yield", this reset won't happen automatically. # Code below checks if today > day when sensor was last updated # and if so: set state to None. # Sensors with live values like "temperature" or "current_power" # will also be reset to None. if not values and ( (sensor.per_day_basis and date.today() > sensor.date_updated) or (not sensor.per_day_basis and not sensor.per_total_basis)): state_unknown = True sensor.async_update_values(unknown_state=state_unknown) return values @callback def start_update_interval(event): """Start the update interval scheduling.""" nonlocal remove_interval_update remove_interval_update = async_track_time_interval_backoff( hass, async_saj) @callback def stop_update_interval(event): """Properly cancel the scheduled update.""" remove_interval_update() # pylint: disable=not-callable hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, stop_update_interval) async_at_start(hass, start_update_interval)
class StatisticsSensor(SensorEntity): """Representation of a Statistics sensor.""" def __init__( self, source_entity_id, name, state_characteristic, samples_max_buffer_size, samples_max_age, precision, quantile_intervals, quantile_method, ): """Initialize the Statistics sensor.""" self._source_entity_id = source_entity_id self.is_binary = self._source_entity_id.split(".")[0] == "binary_sensor" self._name = name self._state_characteristic = state_characteristic if self._state_characteristic == STAT_DEFAULT: self._state_characteristic = STAT_COUNT if self.is_binary else STAT_MEAN _LOGGER.warning(DEPRECATION_WARNING, self._state_characteristic, name) self._samples_max_buffer_size = samples_max_buffer_size self._samples_max_age = samples_max_age self._precision = precision self._quantile_intervals = quantile_intervals self._quantile_method = quantile_method self._value = None self._unit_of_measurement = None self._available = False self.states = deque(maxlen=self._samples_max_buffer_size) self.ages = deque(maxlen=self._samples_max_buffer_size) self.attributes = { STAT_AGE_COVERAGE_RATIO: None, STAT_BUFFER_USAGE_RATIO: None, STAT_SOURCE_VALUE_VALID: None, } if self.is_binary: self._state_characteristic_fn = getattr( self, f"_stat_binary_{self._state_characteristic}" ) else: self._state_characteristic_fn = getattr( self, f"_stat_{self._state_characteristic}" ) self._update_listener = None async def async_added_to_hass(self): """Register callbacks.""" @callback def async_stats_sensor_state_listener(event): """Handle the sensor state changes.""" if (new_state := event.data.get("new_state")) is None: return self._add_state_to_queue(new_state) self.async_schedule_update_ha_state(True) async def async_stats_sensor_startup(_): """Add listener and get recorded state.""" _LOGGER.debug("Startup for %s", self.entity_id) self.async_on_remove( async_track_state_change_event( self.hass, [self._source_entity_id], async_stats_sensor_state_listener, ) ) if "recorder" in self.hass.config.components: self.hass.async_create_task(self._initialize_from_database()) async_at_start(self.hass, async_stats_sensor_startup)
class StatisticsSensor(SensorEntity): """Representation of a Statistics sensor.""" def __init__( self, source_entity_id: str, name: str, unique_id: str | None, state_characteristic: str, samples_max_buffer_size: int, samples_max_age: timedelta | None, precision: int, quantile_intervals: int, quantile_method: Literal["exclusive", "inclusive"], ) -> None: """Initialize the Statistics sensor.""" self._attr_icon: str = ICON self._attr_name: str = name self._attr_should_poll: bool = False self._attr_unique_id: str | None = unique_id self._source_entity_id: str = source_entity_id self.is_binary: bool = (split_entity_id( self._source_entity_id)[0] == BINARY_SENSOR_DOMAIN) self._state_characteristic: str = state_characteristic self._samples_max_buffer_size: int = samples_max_buffer_size self._samples_max_age: timedelta | None = samples_max_age self._precision: int = precision self._quantile_intervals: int = quantile_intervals self._quantile_method: Literal["exclusive", "inclusive"] = quantile_method self._value: StateType | datetime = None self._unit_of_measurement: str | None = None self._available: bool = False self.states: deque[float | bool] = deque( maxlen=self._samples_max_buffer_size) self.ages: deque[datetime] = deque( maxlen=self._samples_max_buffer_size) self.attributes: dict[str, StateType] = { STAT_AGE_COVERAGE_RATIO: None, STAT_BUFFER_USAGE_RATIO: None, STAT_SOURCE_VALUE_VALID: None, } self._state_characteristic_fn: Callable[[], StateType | datetime] if self.is_binary: self._state_characteristic_fn = getattr( self, f"_stat_binary_{self._state_characteristic}") else: self._state_characteristic_fn = getattr( self, f"_stat_{self._state_characteristic}") self._update_listener: CALLBACK_TYPE | None = None async def async_added_to_hass(self) -> None: """Register callbacks.""" @callback def async_stats_sensor_state_listener(event: Event) -> None: """Handle the sensor state changes.""" if (new_state := event.data.get("new_state")) is None: return self._add_state_to_queue(new_state) self.async_schedule_update_ha_state(True) async def async_stats_sensor_startup(_: HomeAssistant) -> None: """Add listener and get recorded state.""" _LOGGER.debug("Startup for %s", self.entity_id) self.async_on_remove( async_track_state_change_event( self.hass, [self._source_entity_id], async_stats_sensor_state_listener, )) if "recorder" in self.hass.config.components: self.hass.async_create_task(self._initialize_from_database()) self.async_on_remove( async_at_start(self.hass, async_stats_sensor_startup))
async def async_added_to_hass(self): """Handle addition to Home Assistant.""" self.async_on_remove(start.async_at_start(self.hass, self._async_start))
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up alerts.""" last_alerts: dict[str, str | None] = {} async def async_update_alerts() -> None: nonlocal last_alerts active_alerts: dict[str, str | None] = {} for issue_id, alert in coordinator.data.items(): # Skip creation if already created and not updated since then if issue_id in last_alerts and alert.date_updated == last_alerts[ issue_id]: active_alerts[issue_id] = alert.date_updated continue # Fetch alert to get title + description try: response = await async_get_clientsession(hass).get( f"https://alerts.home-assistant.io/alerts/{alert.filename}", timeout=aiohttp.ClientTimeout(total=10), ) except asyncio.TimeoutError: _LOGGER.warning("Error fetching %s: timeout", alert.filename) continue alert_content = await response.text() alert_parts = alert_content.split("---") if len(alert_parts) != 3: _LOGGER.warning("Error parsing %s: unexpected metadata format", alert.filename) continue try: alert_info = parse_yaml(alert_parts[1]) except ValueError as err: _LOGGER.warning("Error parsing %s metadata: %s", alert.filename, err) continue if not isinstance(alert_info, dict) or "title" not in alert_info: _LOGGER.warning("Error in %s metadata: title not found", alert.filename) continue alert_title = alert_info["title"] alert_content = alert_parts[2].strip() async_create_issue( hass, DOMAIN, issue_id, is_fixable=False, issue_domain=alert.integration, severity=IssueSeverity.WARNING, translation_key="alert", translation_placeholders={ "title": alert_title, "description": alert_content, }, ) active_alerts[issue_id] = alert.date_updated inactive_alerts = last_alerts.keys() - active_alerts.keys() for issue_id in inactive_alerts: async_delete_issue(hass, DOMAIN, issue_id) last_alerts = active_alerts @callback def async_schedule_update_alerts() -> None: if not coordinator.last_update_success: return hass.async_create_task(async_update_alerts()) coordinator = AlertUpdateCoordinator(hass) coordinator.async_add_listener(async_schedule_update_alerts) async def initial_refresh(hass: HomeAssistant) -> None: await coordinator.async_refresh() async_at_start(hass, initial_refresh) return True