-
-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #72 from Alexwijn/develop
New Years Eve Release
- Loading branch information
Showing
53 changed files
with
3,986 additions
and
1,785 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
name: Release | ||
|
||
on: | ||
release: | ||
types: [ published ] | ||
|
||
jobs: | ||
release: | ||
|
||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: actions/checkout@v2 | ||
- uses: vimtor/[email protected] | ||
with: | ||
files: custom_components/ | ||
dest: sat.zip |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
[MASTER] | ||
# Specify a configuration file. | ||
rcfile= | ||
|
||
[REPORTS] | ||
# Set the output format. Available formats: text, parseable, colorized, msvs (visual studio). | ||
output-format=text | ||
|
||
[FORMAT] | ||
# Maximum number of characters on a single line. | ||
max-line-length=200 | ||
|
||
[DESIGN] | ||
# Maximum number of arguments for function / method. | ||
max-args=5 | ||
|
||
[TYPECHECK] | ||
# Tells whether missing members accessed in mixin class should be ignored. | ||
ignore-mixin-members=yes |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,151 +1,227 @@ | ||
import asyncio | ||
import logging | ||
import traceback | ||
|
||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN | ||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN | ||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN | ||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN | ||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.helpers import device_registry | ||
from homeassistant.helpers.storage import Store | ||
|
||
from . import mqtt, serial, switch | ||
from .const import * | ||
from sentry_sdk import Client, Hub | ||
|
||
from .const import ( | ||
DOMAIN, | ||
CLIMATE, | ||
SENTRY, | ||
COORDINATOR, | ||
CONF_MODE, | ||
CONF_DEVICE, | ||
CONF_ERROR_MONITORING, OPTIONS_DEFAULTS, | ||
) | ||
from .coordinator import SatDataUpdateCoordinatorFactory | ||
|
||
_LOGGER: logging.Logger = logging.getLogger(__name__) | ||
PLATFORMS = [CLIMATE_DOMAIN, SENSOR_DOMAIN, NUMBER_DOMAIN, BINARY_SENSOR_DOMAIN] | ||
|
||
|
||
async def async_setup_entry(_hass: HomeAssistant, _entry: ConfigEntry): | ||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): | ||
""" | ||
Set up this integration using the UI. | ||
This function is called by Home Assistant when the integration is set up with the UI. | ||
""" | ||
# Make sure we have our default domain property | ||
_hass.data.setdefault(DOMAIN, {}) | ||
hass.data.setdefault(DOMAIN, {}) | ||
|
||
# Create a new dictionary for this entry | ||
_hass.data[DOMAIN][_entry.entry_id] = {} | ||
hass.data[DOMAIN][entry.entry_id] = {} | ||
|
||
# Setup error monitoring (if enabled) | ||
if entry.options.get(CONF_ERROR_MONITORING, OPTIONS_DEFAULTS[CONF_ERROR_MONITORING]): | ||
await hass.async_add_executor_job(initialize_sentry, hass) | ||
|
||
# Resolve the coordinator by using the factory according to the mode | ||
_hass.data[DOMAIN][_entry.entry_id][COORDINATOR] = await SatDataUpdateCoordinatorFactory().resolve( | ||
hass=_hass, data=_entry.data, options=_entry.options, mode=_entry.data.get(CONF_MODE), device=_entry.data.get(CONF_DEVICE) | ||
hass.data[DOMAIN][entry.entry_id][COORDINATOR] = SatDataUpdateCoordinatorFactory().resolve( | ||
hass=hass, data=entry.data, options=entry.options, mode=entry.data.get(CONF_MODE), device=entry.data.get(CONF_DEVICE) | ||
) | ||
|
||
# Forward entry setup for climate and other platforms | ||
await _hass.async_add_job(_hass.config_entries.async_forward_entry_setup(_entry, CLIMATE_DOMAIN)) | ||
await _hass.async_add_job(_hass.config_entries.async_forward_entry_setups(_entry, [SENSOR_DOMAIN, NUMBER_DOMAIN, BINARY_SENSOR_DOMAIN])) | ||
# Making sure everything is loaded | ||
await hass.data[DOMAIN][entry.entry_id][COORDINATOR].async_setup() | ||
|
||
# Forward entry setup for used platforms | ||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
|
||
# Add an update listener for this entry | ||
_entry.async_on_unload(_entry.add_update_listener(async_reload_entry)) | ||
entry.async_on_unload(entry.add_update_listener(async_reload_entry)) | ||
|
||
return True | ||
|
||
|
||
async def async_unload_entry(_hass: HomeAssistant, _entry: ConfigEntry) -> bool: | ||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
""" | ||
Handle removal of an entry. | ||
This function is called by Home Assistant when the integration is being removed. | ||
""" | ||
|
||
climate = _hass.data[DOMAIN][_entry.entry_id][CLIMATE] | ||
await _hass.data[DOMAIN][_entry.entry_id][COORDINATOR].async_will_remove_from_hass(climate) | ||
_climate = hass.data[DOMAIN][entry.entry_id][CLIMATE] | ||
_coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] | ||
|
||
await _coordinator.async_will_remove_from_hass() | ||
|
||
unloaded = all( | ||
await asyncio.gather( | ||
_hass.config_entries.async_unload_platforms(_entry, [CLIMATE_DOMAIN, SENSOR_DOMAIN, NUMBER_DOMAIN, BINARY_SENSOR_DOMAIN]), | ||
) | ||
# Forward entry unload for used platforms | ||
await asyncio.gather(hass.config_entries.async_unload_platforms(entry, PLATFORMS)) | ||
) | ||
|
||
try: | ||
if SENTRY in hass.data[DOMAIN]: | ||
hass.data[DOMAIN][SENTRY].flush() | ||
hass.data[DOMAIN][SENTRY].close() | ||
hass.data[DOMAIN].pop(SENTRY, None) | ||
except Exception as ex: | ||
_LOGGER.error("Error during Sentry cleanup: %s", str(ex)) | ||
|
||
# Remove the entry from the data dictionary if all components are unloaded successfully | ||
if unloaded: | ||
_hass.data[DOMAIN].pop(_entry.entry_id) | ||
hass.data[DOMAIN].pop(entry.entry_id) | ||
|
||
return unloaded | ||
|
||
|
||
async def async_reload_entry(_hass: HomeAssistant, _entry: ConfigEntry) -> None: | ||
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: | ||
""" | ||
Reload config entry. | ||
This function is called by Home Assistant when the integration configuration is updated. | ||
""" | ||
# Unload the entry and its dependent components | ||
await async_unload_entry(_hass, _entry) | ||
await async_unload_entry(hass, entry) | ||
|
||
# Set up the entry again | ||
await async_setup_entry(_hass, _entry) | ||
await async_setup_entry(hass, entry) | ||
|
||
|
||
async def async_migrate_entry(_hass: HomeAssistant, _entry: ConfigEntry) -> bool: | ||
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Migrate old entry.""" | ||
from custom_components.sat.config_flow import SatFlowHandler | ||
_LOGGER.debug("Migrating from version %s", _entry.version) | ||
from .config_flow import SatFlowHandler | ||
_LOGGER.debug("Migrating from version %s", entry.version) | ||
|
||
if _entry.version < SatFlowHandler.VERSION: | ||
new_data = {**_entry.data} | ||
new_options = {**_entry.options} | ||
if entry.version < SatFlowHandler.VERSION: | ||
new_data = {**entry.data} | ||
new_options = {**entry.options} | ||
|
||
if _entry.version < 2: | ||
if not _entry.data.get(CONF_MINIMUM_SETPOINT): | ||
if entry.version < 2: | ||
if not entry.data.get("minimum_setpoint"): | ||
# Legacy Store | ||
store = Store(_hass, 1, DOMAIN) | ||
new_data[CONF_MINIMUM_SETPOINT] = MINIMUM_SETPOINT | ||
store = Store(hass, 1, DOMAIN) | ||
new_data["minimum_setpoint"] = 10 | ||
|
||
if (data := await store.async_load()) and (overshoot_protection_value := data.get("overshoot_protection_value")): | ||
new_data[CONF_MINIMUM_SETPOINT] = overshoot_protection_value | ||
new_data["minimum_setpoint"] = overshoot_protection_value | ||
|
||
if _entry.options.get("heating_system") == "underfloor": | ||
new_data[CONF_HEATING_SYSTEM] = HEATING_SYSTEM_UNDERFLOOR | ||
if entry.options.get("heating_system") == "underfloor": | ||
new_data["heating_system"] = "underfloor" | ||
else: | ||
new_data[CONF_HEATING_SYSTEM] = HEATING_SYSTEM_RADIATORS | ||
new_data["heating_system"] = "radiators" | ||
|
||
if not _entry.data.get(CONF_MAXIMUM_SETPOINT): | ||
new_data[CONF_MAXIMUM_SETPOINT] = 55 | ||
if not entry.data.get("maximum_setpoint"): | ||
new_data["maximum_setpoint"] = 55 | ||
|
||
if _entry.options.get("heating_system") == "underfloor": | ||
new_data[CONF_MAXIMUM_SETPOINT] = 50 | ||
if entry.options.get("heating_system") == "underfloor": | ||
new_data["maximum_setpoint"] = 50 | ||
|
||
if _entry.options.get("heating_system") == "radiator_low_temperatures": | ||
new_data[CONF_MAXIMUM_SETPOINT] = 55 | ||
if entry.options.get("heating_system") == "radiator_low_temperatures": | ||
new_data["maximum_setpoint"] = 55 | ||
|
||
if _entry.options.get("heating_system") == "radiator_medium_temperatures": | ||
new_data[CONF_MAXIMUM_SETPOINT] = 65 | ||
if entry.options.get("heating_system") == "radiator_medium_temperatures": | ||
new_data["maximum_setpoint"] = 65 | ||
|
||
if _entry.options.get("heating_system") == "radiator_high_temperatures": | ||
new_data[CONF_MAXIMUM_SETPOINT] = 75 | ||
if entry.options.get("heating_system") == "radiator_high_temperatures": | ||
new_data["maximum_setpoint"] = 75 | ||
|
||
if _entry.version < 3: | ||
if main_climates := _entry.options.get("main_climates"): | ||
new_data[CONF_MAIN_CLIMATES] = main_climates | ||
if entry.version < 3: | ||
if main_climates := entry.options.get("main_climates"): | ||
new_data["main_climates"] = main_climates | ||
new_options.pop("main_climates") | ||
|
||
if secondary_climates := _entry.options.get("climates"): | ||
new_data[CONF_SECONDARY_CLIMATES] = secondary_climates | ||
if secondary_climates := entry.options.get("climates"): | ||
new_data["secondary_climates"] = secondary_climates | ||
new_options.pop("climates") | ||
|
||
if sync_with_thermostat := _entry.options.get("sync_with_thermostat"): | ||
new_data[CONF_SYNC_WITH_THERMOSTAT] = sync_with_thermostat | ||
if sync_with_thermostat := entry.options.get("sync_with_thermostat"): | ||
new_data["sync_with_thermostat"] = sync_with_thermostat | ||
new_options.pop("sync_with_thermostat") | ||
|
||
if _entry.version < 4: | ||
if _entry.data.get("window_sensor") is not None: | ||
new_data[CONF_WINDOW_SENSORS] = [_entry.data.get("window_sensor")] | ||
if entry.version < 4: | ||
if entry.data.get("window_sensor") is not None: | ||
new_data["window_sensors"] = [entry.data.get("window_sensor")] | ||
del new_options["window_sensor"] | ||
|
||
if _entry.version < 5: | ||
if _entry.options.get("overshoot_protection") is not None: | ||
new_data[CONF_OVERSHOOT_PROTECTION] = _entry.options.get("overshoot_protection") | ||
if entry.version < 5: | ||
if entry.options.get("overshoot_protection") is not None: | ||
new_data["overshoot_protection"] = entry.options.get("overshoot_protection") | ||
del new_options["overshoot_protection"] | ||
|
||
if _entry.version < 6: | ||
new_options[CONF_HEATING_CURVE_VERSION] = 1 | ||
if entry.version < 7: | ||
new_options["pid_controller_version"] = 1 | ||
|
||
if entry.version < 8: | ||
if entry.options.get("heating_curve_version") is not None and int(entry.options.get("heating_curve_version")) < 2: | ||
new_options["heating_curve_version"] = 3 | ||
|
||
if entry.version < 9: | ||
if entry.data.get("heating_system") == "heat_pump": | ||
new_options["cycles_per_hour"] = 2 | ||
|
||
if entry.data.get("heating_system") == "radiators": | ||
new_options["cycles_per_hour"] = 3 | ||
|
||
if entry.version < 10: | ||
if entry.data.get("mode") == "mqtt": | ||
device = device_registry.async_get(hass).async_get(entry.data.get("device")) | ||
|
||
new_data["mode"] = "mqtt_opentherm" | ||
new_data["device"] = list(device.identifiers)[0][1] | ||
|
||
_entry.version = SatFlowHandler.VERSION | ||
_hass.config_entries.async_update_entry(_entry, data=new_data, options=new_options) | ||
hass.config_entries.async_update_entry(entry, version=SatFlowHandler.VERSION, data=new_data, options=new_options) | ||
|
||
_LOGGER.info("Migration to version %s successful", _entry.version) | ||
_LOGGER.info("Migration to version %s successful", entry.version) | ||
|
||
return True | ||
|
||
|
||
def initialize_sentry(hass: HomeAssistant): | ||
"""Initialize Sentry synchronously in an offloaded executor job.""" | ||
|
||
def exception_filter(event, hint): | ||
"""Filter events to send only SAT-related exceptions to Sentry.""" | ||
exc_info = hint.get("exc_info") | ||
|
||
if exc_info: | ||
_, _, exc_traceback = exc_info | ||
stack = traceback.extract_tb(exc_traceback) | ||
|
||
# Check if the exception originates from the SAT custom component | ||
if any("custom_components/sat/" in frame.filename for frame in stack): | ||
return event | ||
|
||
# Ignore exceptions not related to SAT | ||
return None | ||
|
||
# Configure the Sentry client | ||
client = Client( | ||
traces_sample_rate=1.0, | ||
before_send=exception_filter, | ||
dsn="https://216fc0a74c488abdb79f9839fb7da33e@o4508432869621760.ingest.de.sentry.io/4508432872898640", | ||
) | ||
|
||
# Bind the Sentry client to the Sentry hub | ||
hub = Hub(client) | ||
hub.bind_client(client) | ||
|
||
# Store the hub in Home Assistant's data for later use | ||
hass.data[DOMAIN][SENTRY] = client |
Oops, something went wrong.