Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 10 additions & 242 deletions custom_components/plugwise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,15 @@

import asyncio
import logging
from datetime import timedelta
from typing import Dict

import async_timeout
import voluptuous as vol
from Plugwise_Smile.Smile import Smile

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_PORT,
CONF_SCAN_INTERVAL,
CONF_USERNAME,
)
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant

from .const import (
ALL_PLATFORMS,
API,
COORDINATOR,
DEFAULT_PORT,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
SENSOR_PLATFORMS,
SERVICE_DELETE,
UNDO_UPDATE_LISTENER,
)
from .const import ALL_PLATFORMS, DOMAIN, UNDO_UPDATE_LISTENER
from .gateway import async_setup_entry_gw

CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)

Expand All @@ -48,161 +21,12 @@ async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Plugwise platform."""
return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Plugwise Smiles from a config entry."""
websession = async_get_clientsession(hass, verify_ssl=False)

# When migrating from Core to beta, add the username to ConfigEntry
entry_updates = {}
try:
username = entry.data[CONF_USERNAME]
except KeyError:
username = DEFAULT_USERNAME
data = {**entry.data}
data.update({"username": username})
entry_updates["data"] = data

if entry_updates:
hass.config_entries.async_update_entry(entry, **entry_updates)

api = Smile(
host=entry.data[CONF_HOST],
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
port=entry.data.get(CONF_PORT, DEFAULT_PORT),
timeout=30,
websession=websession,
)

try:
connected = await api.connect()

if not connected:
_LOGGER.error("Unable to connect to Smile %s", smile_name)
raise ConfigEntryNotReady

except Smile.InvalidAuthentication:
_LOGGER.error("Invalid username or Smile ID")
return False

except Smile.PlugwiseError as err:
_LOGGER.error("Error while communicating to Smile %s", api.smile_name)
raise ConfigEntryNotReady from err

except asyncio.TimeoutError as err:
_LOGGER.error("Timeout while connecting to Smile %s", api.smile_name)
raise ConfigEntryNotReady from err

update_interval = timedelta(
seconds=entry.options.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL[api.smile_type]
)
)

async def async_update_data():
"""Update data via API endpoint."""
_LOGGER.debug("Updating Smile %s", api.smile_name)
try:
async with async_timeout.timeout(60):
await api.full_update_device()
_LOGGER.debug("Succesfully updated Smile %s", api.smile_name)
return True
except Smile.XMLDataMissingError as err:
_LOGGER.debug(
"Updating Smile failed, expected XML data for %s", api.smile_name
)
raise UpdateFailed("Smile update failed") from err
except Smile.PlugwiseError as err:
_LOGGER.debug(
"Updating Smile failed, generic failure for %s", api.smile_name
)
raise UpdateFailed("Smile update failed") from err

coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=f"Smile {api.smile_name}",
update_method=async_update_data,
update_interval=update_interval,
)

await coordinator.async_refresh()

if not coordinator.last_update_success:
raise ConfigEntryNotReady

_LOGGER.debug("Async update interval %s", update_interval)

api.get_all_devices()

undo_listener = entry.add_update_listener(_update_listener)

# Migrate to a valid unique_id when needed
if entry.unique_id is None:
if api.smile_version[0] != "1.8.0":
hass.config_entries.async_update_entry(entry, unique_id=api.smile_hostname)

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
API: api,
COORDINATOR: coordinator,
UNDO_UPDATE_LISTENER: undo_listener,
}

_LOGGER.debug("Gateway is %s", api.gateway_id)

_LOGGER.debug("Gateway sofware version is %s", api.smile_version)
_LOGGER.debug("Appliances is %s", api.get_all_appliances())
_LOGGER.debug("Scan thermostats is %s", api.scan_thermostats())
_LOGGER.debug("Locations (matched) is %s", api.match_locations())

device_registry = await dr.async_get_registry(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, api.gateway_id)},
manufacturer="Plugwise",
name=entry.title,
model=f"Smile {api.smile_name}",
sw_version=api.smile_version[0],
)

single_master_thermostat = api.single_master_thermostat()
_LOGGER.debug("Single master thermostat = %s", single_master_thermostat)

platforms = ALL_PLATFORMS
if single_master_thermostat is None:
platforms = SENSOR_PLATFORMS

async def async_delete_notification(self):
"""Service: delete the Plugwise Notification."""
_LOGGER.debug("Service delete PW Notification called for %s", api.smile_name)
try:
deleted = await api.delete_notification()
_LOGGER.debug("PW Notification deleted: %s", deleted)
except Smile.PlugwiseError:
_LOGGER.debug(
"Failed to delete the Plugwise Notification for %s", api.smile_name
)

for component in platforms:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
if component == "climate":
hass.services.async_register(
DOMAIN, SERVICE_DELETE, async_delete_notification, schema=vol.Schema({})
)

return True


async def _update_listener(hass: HomeAssistant, entry: ConfigEntry):
"""Handle options update."""
coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR]
coordinator.update_interval = timedelta(
seconds=entry.options.get(CONF_SCAN_INTERVAL)
)

"""Set up Plugwise components from a config entry."""
if entry.data.get(CONF_HOST):
return await async_setup_entry_gw(hass, entry)
# PLACEHOLDER USB entry setup
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True if we don't want to include #117 just yet (which was the goal, right)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's the idea, first, implement the structure-change from Core. Next step, implement #117.

return False

async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
Expand All @@ -220,60 +44,4 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok


class SmileGateway(CoordinatorEntity):
"""Represent Smile Gateway."""

def __init__(self, api, coordinator, name, dev_id):
"""Initialise the gateway."""

super().__init__(coordinator)
self._api = api
self._name = name
self._dev_id = dev_id

self._unique_id = None
self._model = None

self._entity_name = self._name

@property
def unique_id(self):
"""Return a unique ID."""
return self._unique_id

@property
def name(self):
"""Return the name of the entity, if any."""
return self._name

@property
def device_info(self) -> Dict[str, any]:
"""Return the device information."""
device_information = {
"identifiers": {(DOMAIN, self._dev_id)},
"name": self._entity_name,
"manufacturer": "Plugwise",
}

if self._model is not None:
device_information["model"] = self._model.replace("_", " ").title()

if self._dev_id != self._api.gateway_id:
device_information["via_device"] = (DOMAIN, self._api.gateway_id)

return device_information

async def async_added_to_hass(self):
"""Subscribe to updates."""
self._async_process_data()
self.async_on_remove(
self.coordinator.async_add_listener(self._async_process_data)
)

@callback
def _async_process_data(self):
"""Interpret and process API data."""
raise NotImplementedError
return unload_ok
2 changes: 1 addition & 1 deletion custom_components/plugwise/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from homeassistant.core import callback

from . import SmileGateway
from .gateway import SmileGateway
from .const import (
API,
COORDINATOR,
Expand Down
42 changes: 26 additions & 16 deletions custom_components/plugwise/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,34 +24,34 @@
DEFAULT_SCAN_INTERVAL,
DOMAIN,
ZEROCONF_MAP,
) # pylint:disable=unused-import
)

_LOGGER = logging.getLogger(__name__)


def _base_schema(discovery_info):
"""Generate base schema."""
base_schema = {}
def _base_gw_schema(discovery_info):
"""Generate base schema for gateways."""
base_gw_schema = {}

if not discovery_info:
base_schema[vol.Required(CONF_HOST)] = str
base_schema[vol.Optional(CONF_PORT, default=DEFAULT_PORT)] = int
base_gw_schema[vol.Required(CONF_HOST)] = str
base_gw_schema[vol.Optional(CONF_PORT, default=DEFAULT_PORT)] = int

base_schema.update(
base_gw_schema.update(
{
vol.Required(CONF_USERNAME, description={"suggested_value": "smile"}): str,
vol.Required(CONF_PASSWORD): str,
}
)

return vol.Schema(base_schema)
return vol.Schema(base_gw_schema)


async def validate_input(hass: core.HomeAssistant, data):
async def validate_gw_input(hass: core.HomeAssistant, data):
"""
Validate whether the user input allows us to connect.
Validate whether the user input allows us to connect to the gateway.

Data has the keys from _base_schema() with values provided by the user.
Data has the keys from _base_gw_schema() with values provided by the user.
"""
websession = async_get_clientsession(hass, verify_ssl=False)

Expand All @@ -74,6 +74,9 @@ async def validate_input(hass: core.HomeAssistant, data):
return api


# PLACEHOLDER USB connection validation


class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Plugwise Smile."""

Expand Down Expand Up @@ -108,8 +111,8 @@ async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType):
}
return await self.async_step_user()

async def async_step_user(self, user_input=None):
"""Handle the initial step."""
async def async_step_user_gateway(self, user_input=None):
"""Handle the initial step for gateways."""
errors = {}

if user_input is not None:
Expand All @@ -123,7 +126,7 @@ async def async_step_user(self, user_input=None):
return self.async_abort(reason="already_configured")

try:
api = await validate_input(self.hass, user_input)
api = await validate_gw_input(self.hass, user_input)

except CannotConnect:
errors[CONF_BASE] = "cannot_connect"
Expand All @@ -142,11 +145,18 @@ async def async_step_user(self, user_input=None):
return self.async_create_entry(title=api.smile_name, data=user_input)

return self.async_show_form(
step_id="user",
data_schema=_base_schema(self.discovery_info),
step_id="user_gateway",
data_schema=_base_gw_schema(self.discovery_info),
errors=errors or {},
)

# PLACEHOLDER USB async_step_user_usb and async_step_user_usb_manual_paht

async def async_step_user(self, user_input=None):
"""Handle the initial step."""
# PLACEHOLDER USB vs Gateway Logic
return await self.async_step_user_gateway()

@staticmethod
@callback
def async_get_options_flow(config_entry):
Expand Down
Loading