This repository has been archived by the owner on Aug 15, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 36
Add Switch entity for One Time Charge #108
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
cc44b18
Add Switch entity for One Time Charge
lukx f75ec26
improve user experience by ignoring setback updates for a while after…
lukx 3635acc
Fixing some logs, and refactoring the timedelta mechanism.
lukx 81a7320
Merge branch 'master' into deactivate-onetimecharge
lukx 05c923f
Merge branch 'master' of https://github.com/oischinger/ha_vicare into…
lukx 112774b
update to new ID concepts
lukx File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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 |
---|---|---|
@@ -0,0 +1,211 @@ | ||
"""Viessmann ViCare switch device.""" | ||
from __future__ import annotations | ||
|
||
import datetime | ||
import logging | ||
from contextlib import suppress | ||
from dataclasses import dataclass | ||
|
||
import requests | ||
from PyViCare.PyViCareUtils import ( | ||
PyViCareInternalServerError, | ||
PyViCareInvalidDataError, | ||
PyViCareNotSupportedFeatureError, | ||
PyViCareRateLimitError, | ||
) | ||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription | ||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import EntityCategory | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.helpers.entity import DeviceInfo | ||
from homeassistant.helpers.entity_platform import AddEntitiesCallback | ||
|
||
from . import ViCareRequiredKeysMixin, ViCareToggleKeysMixin | ||
from .const import DOMAIN, VICARE_DEVICE_CONFIG, VICARE_NAME | ||
from .helpers import get_device_name, get_unique_device_id, get_unique_id | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
SWITCH_DHW_ONETIME_CHARGE = "dhw_onetimecharge" | ||
TIMEDELTA_UPDATE = datetime.timedelta(seconds=5) | ||
|
||
|
||
@dataclass | ||
class ViCareSwitchEntityDescription(SwitchEntityDescription, ViCareRequiredKeysMixin, ViCareToggleKeysMixin): | ||
"""Describes ViCare switch entity.""" | ||
|
||
|
||
SWITCH_DESCRIPTIONS: tuple[ViCareSwitchEntityDescription, ...] = ( | ||
ViCareSwitchEntityDescription( | ||
key=SWITCH_DHW_ONETIME_CHARGE, | ||
name="Activate one-time charge", | ||
icon="mdi:shower-head", | ||
entity_category=EntityCategory.CONFIG, | ||
value_getter=lambda api: api.getOneTimeCharge(), | ||
enabler=lambda api: api.activateOneTimeCharge(), | ||
disabler=lambda api: api.deactivateOneTimeCharge(), | ||
), | ||
) | ||
|
||
|
||
def _build_entity(name, vicare_api, device_config, description): | ||
"""Create a ViCare switch entity.""" | ||
_LOGGER.debug("Found device %s", name) | ||
try: | ||
description.value_getter(vicare_api) | ||
_LOGGER.debug("Found entity %s", name) | ||
except PyViCareInternalServerError as server_error: | ||
_LOGGER.info( | ||
"Server error ( %s): Not creating entity %s", server_error.message, name | ||
) | ||
return None | ||
except PyViCareNotSupportedFeatureError: | ||
_LOGGER.info("Feature not supported %s", name) | ||
return None | ||
except AttributeError: | ||
_LOGGER.debug("Attribute Error %s", name) | ||
return None | ||
|
||
return ViCareSwitch( | ||
name, | ||
vicare_api, | ||
device_config, | ||
description, | ||
) | ||
|
||
|
||
async def async_setup_entry( | ||
hass: HomeAssistant, | ||
config_entry: ConfigEntry, | ||
async_add_entities: AddEntitiesCallback, | ||
) -> None: | ||
"""Create the ViCare switch entities.""" | ||
entities = await hass.async_add_executor_job( | ||
create_all_entities, hass, config_entry | ||
) | ||
async_add_entities(entities) | ||
|
||
|
||
def create_all_entities(hass: HomeAssistant, config_entry: ConfigEntry): | ||
"""Create entities for all devices and their circuits, burners or compressors if applicable.""" | ||
name = VICARE_NAME | ||
entities = [] | ||
|
||
for device in hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG]: | ||
api = device.asAutoDetectDevice() | ||
|
||
for description in SWITCH_DESCRIPTIONS: | ||
entity = _build_entity( | ||
f"{name} {description.name}", | ||
api, | ||
device, | ||
description, | ||
) | ||
if entity is not None: | ||
entities.append(entity) | ||
|
||
return entities | ||
|
||
|
||
class ViCareSwitch(SwitchEntity): | ||
"""Representation of a ViCare switch.""" | ||
|
||
entity_description: ViCareSwitchEntityDescription | ||
|
||
def __init__( | ||
self, name, api, device_config, description: ViCareSwitchEntityDescription | ||
) -> None: | ||
"""Initialize the switch.""" | ||
self.entity_description = description | ||
self._device_config = device_config | ||
self._api = api | ||
self._ignore_update_until = datetime.datetime.utcnow() | ||
self._state = None | ||
|
||
@property | ||
def is_on(self) -> bool: | ||
"""Return true if device is on.""" | ||
return self._state | ||
|
||
async def async_update(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we should make this method async. 99% of the work in this method needs to be in an executor job anyway. Also we could more easily retrieve the initial state (which we have in PyVicare's Cache anyway during startup) by replacing |
||
"""update internal state""" | ||
now = datetime.datetime.utcnow() | ||
"""we have identified that the API does not directly sync the represented state, therefore we want to keep | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we might change this behavior in the future. I think we should trigger an update after sending a command on the API. But for now (since we don't have that mechanism yet) let's keep this |
||
an assumed state for a couple of seconds - so lets ignore an update | ||
""" | ||
if now < self._ignore_update_until: | ||
_LOGGER.debug("Ignoring Update Request for OneTime Charging for some seconds") | ||
return | ||
|
||
try: | ||
with suppress(PyViCareNotSupportedFeatureError): | ||
_LOGGER.debug("Fetching DHW One Time Charging Status") | ||
self._state = await self.hass.async_add_executor_job(self.entity_description.value_getter, self._api) | ||
except requests.exceptions.ConnectionError: | ||
_LOGGER.error("Unable to retrieve data from ViCare server") | ||
except ValueError: | ||
_LOGGER.error("Unable to decode data from ViCare server") | ||
except PyViCareRateLimitError as limit_exception: | ||
_LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception) | ||
except PyViCareInvalidDataError as invalid_data_exception: | ||
_LOGGER.error("Invalid data from Vicare server: %s", invalid_data_exception) | ||
|
||
async def async_turn_on(self, **kwargs: Any) -> None: | ||
"""Handle the button press.""" | ||
try: | ||
with suppress(PyViCareNotSupportedFeatureError): | ||
"""Turn the switch on.""" | ||
_LOGGER.debug("Enabling DHW One-Time-Charging") | ||
await self.hass.async_add_executor_job(self.entity_description.enabler, self._api) | ||
self._ignore_update_until = datetime.datetime.utcnow() + TIMEDELTA_UPDATE | ||
self._state = True | ||
|
||
except requests.exceptions.ConnectionError: | ||
_LOGGER.error("Unable to retrieve data from ViCare server") | ||
except ValueError: | ||
_LOGGER.error("Unable to decode data from ViCare server") | ||
except PyViCareRateLimitError as limit_exception: | ||
_LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception) | ||
except PyViCareInvalidDataError as invalid_data_exception: | ||
_LOGGER.error("Invalid data from Vicare server: %s", invalid_data_exception) | ||
|
||
async def async_turn_off(self, **kwargs: Any) -> None: | ||
"""Handle the button press.""" | ||
try: | ||
with suppress(PyViCareNotSupportedFeatureError): | ||
_LOGGER.debug("Disabling DHW One-Time-Charging") | ||
await self.hass.async_add_executor_job(self.entity_description.disabler, self._api) | ||
self._ignore_update_until = datetime.datetime.utcnow() + TIMEDELTA_UPDATE | ||
self._state = False | ||
|
||
except requests.exceptions.ConnectionError: | ||
_LOGGER.error("Unable to retrieve data from ViCare server") | ||
except ValueError: | ||
_LOGGER.error("Unable to decode data from ViCare server") | ||
except PyViCareRateLimitError as limit_exception: | ||
_LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception) | ||
except PyViCareInvalidDataError as invalid_data_exception: | ||
_LOGGER.error("Invalid data from Vicare server: %s", invalid_data_exception) | ||
|
||
@property | ||
def device_info(self) -> DeviceInfo: | ||
"""Return device info for this device.""" | ||
return DeviceInfo( | ||
identifiers={ | ||
( | ||
DOMAIN, | ||
get_unique_device_id(self._device_config), | ||
) | ||
}, | ||
name=get_device_name(self._device_config), | ||
manufacturer="Viessmann", | ||
model=self._device_config.getModel(), | ||
configuration_url="https://developer.viessmann.com/", | ||
) | ||
|
||
@property | ||
def unique_id(self) -> str: | ||
"""Return unique ID for this device.""" | ||
return get_unique_id( | ||
self._api, self._device_config, self.entity_description.key | ||
) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please replace
self._state = None
in withupdate()
like in the other integrations. This way we can also write tests which assert the state.See comment below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lukx Can you please have a look at this change request? - I'm using your custom repo at the moment to control the heating of water when the solar panels are producing too much, but seems I can't log-in anymore using your repo. (401: Token missing. Could be unrelated) - But I still think it'd be better to get this integrated in the official repo :)
I've sent you a PR with the change, have a look and just approve :)
Thanks!