Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v3.1.6 #303

Merged
merged 30 commits into from
Sep 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
1ba39f6
Replace deprecated constants
msp1974 Sep 7, 2022
b35c373
Remove unused imports
msp1974 Sep 7, 2022
00f21dd
remove old image
msp1974 Sep 7, 2022
0dca0f9
Allow hot water boost service to use boost button entity
msp1974 Sep 12, 2022
31c965b
remove hacs as requirement for component
msp1974 Sep 12, 2022
c46fc3f
3.1.6
msp1974 Sep 12, 2022
4b2ff67
remove use of hotwater boost button in wiser.boost_hotwater service
msp1974 Sep 12, 2022
4059116
update readme
msp1974 Sep 12, 2022
3944fe4
LTS target temp show None if Off
msp1974 Sep 18, 2022
35aa581
correct ColorMode values
msp1974 Sep 18, 2022
889c9b6
update readme
msp1974 Sep 18, 2022
13d6bfa
LTS sensor set to off instead of -20C
msp1974 Sep 18, 2022
dccb9ef
Replace deprecated SUPPORT constants
msp1974 Sep 20, 2022
a65dd7f
Delete devices via UI and remove orphan service
msp1974 Sep 20, 2022
56394de
update readme
msp1974 Sep 20, 2022
47694aa
remove no longer needed imports
msp1974 Sep 20, 2022
63311f9
move services to own file
msp1974 Sep 20, 2022
b97928e
remove no longer needed imports
msp1974 Sep 20, 2022
95c0140
update readme
msp1974 Sep 20, 2022
bc64b13
Only add LTS sensors if room has devices
msp1974 Sep 21, 2022
014ded2
updated readme
msp1974 Sep 21, 2022
91103e8
remove blank line
msp1974 Sep 21, 2022
d8e773c
make boost_hotwater service not require entity
msp1974 Sep 21, 2022
813dd3c
Add additional attribute values to WiserSystemCircuitState sensor
msp1974 Sep 26, 2022
eba576e
bump api to 0.0.40
msp1974 Sep 27, 2022
bdb9a82
Added schedule next datetime attribute
msp1974 Sep 27, 2022
a960b8e
improve service documentation
msp1974 Sep 27, 2022
9a89e16
update readme files
msp1974 Sep 27, 2022
8a1152c
updated readme
msp1974 Sep 27, 2022
f4a3e83
update readme
msp1974 Sep 27, 2022
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
653 changes: 344 additions & 309 deletions README.md

Large diffs are not rendered by default.

277 changes: 13 additions & 264 deletions custom_components/wiser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import logging
import json
import voluptuous as vol

from wiserHeatAPIv2.wiserhub import (
TEMP_MINIMUM,
TEMP_MAXIMUM,
Expand All @@ -19,32 +20,27 @@
)

from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_MODE,
CONF_HOST,
CONF_MINIMUM,
CONF_NAME,
CONF_PASSWORD,
CONF_SCAN_INTERVAL,
)
from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.entity_registry import (
async_entries_for_device,
)

from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.util import Throttle

from .websockets import async_register_websockets

from .frontend import WiserCardRegistration
from .helpers import get_device_name, get_identifier
from .services import async_setup_services
from .websockets import async_register_websockets

from .const import (
CONF_MOMENTS,
Expand All @@ -69,58 +65,10 @@
WISER_SERVICES
)

from .helpers import get_device_name, get_identifier

_LOGGER = logging.getLogger(__name__)

MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)

ATTR_FILENAME = "filename"
ATTR_TO_ENTITY_ID = "to_entity_id"
ATTR_SCHEDULE_ID = "schedule_id"
CONF_HUB_ID = "wiser_hub_id"
CONF_ENDPOINT = "wiser_hub_endpoint"
SERVICE_REMOVE_ORPHANED_ENTRIES = "remove_orphaned_entries"
SERVICE_OUTPUT_HUB_JSON = "output_hub_json"

GET_SCHEDULE_SCHEMA = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_FILENAME, default=""): vol.Coerce(str),
}
)

SET_SCHEDULE_SCHEMA = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_FILENAME): vol.Coerce(str),
}
)

COPY_SCHEDULE_SCHEMA = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
vol.Required(ATTR_TO_ENTITY_ID): cv.entity_ids,
}
)

ASSIGN_SCHEDULE_SCHEMA = vol.Schema(
{
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
vol.Optional(ATTR_SCHEDULE_ID): vol.Coerce(int),
vol.Required(ATTR_TO_ENTITY_ID): cv.entity_ids,
}
)

SET_DEVICE_MODE_SCHEMA = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_MODE): vol.Coerce(str),
}
)

SELECT_HUB_SCHEMA = vol.All(vol.Schema({vol.Required(CONF_HUB_ID): str}))

CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
Expand Down Expand Up @@ -205,172 +153,8 @@ async def async_setup_entry(hass, config_entry):
# Setup websocket services for frontend cards
await async_register_websockets(hass, data)

# Initialise global services
def get_entity_from_entity_id(entity: str):
"""Get wiser entity from entity_id"""
domain = entity.split(".", 1)[0]
entity_comp = hass.data.get("entity_components", {}).get(domain)
if entity_comp:
return entity_comp.get_entity(entity)
return None

@callback
def get_schedule(service_call):
"""Handle the service call."""
entity_ids = service_call.data[ATTR_ENTITY_ID]
for entity_id in entity_ids:
filename = (
service_call.data[ATTR_FILENAME]
if service_call.data[ATTR_FILENAME] != ""
else (hass.config.config_dir + "/schedules/schedule_" + entity_id.split(".", 1)[1] + ".yaml")
)
entity = get_entity_from_entity_id(entity_id)
if entity:
if hasattr(entity, "get_schedule"):
getattr(entity, "get_schedule")(filename)
else:
_LOGGER.error(f"Cannot save schedule from entity {entity_id}. Please see integration instructions for entities to choose")
else:
_LOGGER.error(f"Invalid entity. {entity_id} does not exist in this integration")

@callback
def set_schedule(service_call):
"""Handle the service call."""
entity_ids = service_call.data[ATTR_ENTITY_ID]
for entity_id in entity_ids:
filename = service_call.data[ATTR_FILENAME]
entity = get_entity_from_entity_id(entity_id)
if entity:
if hasattr(entity, "set_schedule"):
getattr(entity, "set_schedule")(filename)
else:
_LOGGER.error(f"Cannot set schedule for entity {entity_id}. Please see integration instructions for entities to choose")
else:
_LOGGER.error(f"Invalid entity. {entity_id} does not exist in this integration")

@callback
def copy_schedule(service_call):
"""Handle the service call"""
entity_id = service_call.data[ATTR_ENTITY_ID]
to_entity_ids = service_call.data[ATTR_TO_ENTITY_ID]
for to_entity_id in to_entity_ids:
from_entity = get_entity_from_entity_id(entity_id)
to_entity = get_entity_from_entity_id(to_entity_id)

if from_entity and to_entity:
# Check from entity is a schedule entity
if hasattr(from_entity, "copy_schedule"):
getattr(from_entity, "copy_schedule")(to_entity)
else:
_LOGGER.error(f"Cannot copy schedule from entity {from_entity.name}. Please see integration instructions for entities to choose")
else:
_LOGGER.error(f"Invalid entity - {entity_id if not from_entity else ''}{' and ' if not from_entity and not to_entity else ''}{to_entity_id if not to_entity else ''} does not exist in this integration")
return False

@callback
def assign_schedule(service_call):
"""Handle the service call"""
entity_id = service_call.data.get(ATTR_ENTITY_ID)
schedule_id = service_call.data.get(ATTR_SCHEDULE_ID)
to_entity_ids = service_call.data[ATTR_TO_ENTITY_ID]

if entity_id:
# Assign schedule from this entity to another
for to_entity_id in to_entity_ids:
from_entity = get_entity_from_entity_id(entity_id)
to_entity = get_entity_from_entity_id(to_entity_id)

if from_entity and to_entity:
if hasattr(from_entity, "assign_schedule_to_another_entity"):
getattr(from_entity, "assign_schedule_to_another_entity")(to_entity)
else:
_LOGGER.error(f"Cannot assign schedule from entity {from_entity.name}. Please see integration instructions for entities to choose")
else:
_LOGGER.error(f"Invalid entity - {entity_id if not from_entity else ''}{' and ' if not from_entity and not to_entity else ''}{to_entity_id if not to_entity else ''} does not exist in this integration")
elif schedule_id:
# Assign scheduel with id to this entity
for to_entity_id in to_entity_ids:
to_entity = get_entity_from_entity_id(to_entity_id)
if to_entity:
if hasattr(to_entity, "assign_schedule_by_id"):
getattr(to_entity, "assign_schedule_by_id")(schedule_id)
else:
_LOGGER.error(f"Cannot assign schedule to entity {to_entity.name}. Please see integration instructions for entities to choose")
else:
# Create default schedule and assign to entity
for to_entity_id in to_entity_ids:
entity = get_entity_from_entity_id(to_entity_id)
if hasattr(entity, "create_schedule"):
getattr(entity, "create_schedule")()
else:
_LOGGER.error(f"Cannot assign schedule to entity {to_entity.name}. Please see integration instructions for entities to choose")

@callback
def set_device_mode(service_call):
"""Handle the service call."""
entity_ids = service_call.data[ATTR_ENTITY_ID]
mode = service_call.data[ATTR_MODE]
for entity_id in entity_ids:
entity = get_entity_from_entity_id(entity_id)
if entity:
if hasattr(entity, "set_mode"):
if mode.lower() in [option.lower() for option in entity._options]:
getattr(entity, "set_mode")(mode)
else:
_LOGGER.error(F"{mode} is not a valid mode for this device. Options are {entity._options}")
else:
_LOGGER.error(f"Cannot set mode for entity {entity_id}. Please see integration instructions for entities to choose")
else:
_LOGGER.error(f"Invalid entity. {entity_id} does not exist in this integration")

@callback
def remove_orphaned_entries_service(service):
for entry_id in hass.data[DOMAIN]:
hass.async_create_task(
data.async_remove_orphaned_entries(entry_id, service.data[CONF_HUB_ID])
)

hass.services.async_register(
DOMAIN,
WISER_SERVICES["SERVICE_GET_SCHEDULE"],
get_schedule,
schema=GET_SCHEDULE_SCHEMA,
)

hass.services.async_register(
DOMAIN,
WISER_SERVICES["SERVICE_SET_SCHEDULE"],
set_schedule,
schema=SET_SCHEDULE_SCHEMA,
)

hass.services.async_register(
DOMAIN,
WISER_SERVICES["SERVICE_COPY_SCHEDULE"],
copy_schedule,
schema=COPY_SCHEDULE_SCHEMA,
)

hass.services.async_register(
DOMAIN,
WISER_SERVICES["SERVICE_ASSIGN_SCHEDULE"],
assign_schedule,
schema=ASSIGN_SCHEDULE_SCHEMA,
)

hass.services.async_register(
DOMAIN,
WISER_SERVICES["SERVICE_SET_DEVICE_MODE"],
set_device_mode,
schema=SET_DEVICE_MODE_SCHEMA,
)

hass.services.async_register(
DOMAIN,
SERVICE_REMOVE_ORPHANED_ENTRIES,
remove_orphaned_entries_service,
schema=SELECT_HUB_SCHEMA,
)
# Setup services
await async_setup_services(hass, data)

# Add hub as device
await data.async_update_device_registry()
Expand All @@ -391,6 +175,12 @@ async def _async_update_listener(hass, config_entry):
"""Handle options update."""
await hass.config_entries.async_reload(config_entry.entry_id)

async def async_remove_config_entry_device(hass, config_entry, device_entry) -> bool:
"""Delete device if not entities"""
if device_entry.model == "Controller":
_LOGGER.error("You cannot delete the Wiser Controller device via the device delete method. Please remove the integration instead.")
return False
return True

async def async_unload_entry(hass, config_entry):
"""
Expand All @@ -410,7 +200,6 @@ async def async_unload_entry(hass, config_entry):

# Deregister services
_LOGGER.debug("Unregister Wiser Services")
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_ORPHANED_ENTRIES)
hass.services.async_remove(DOMAIN, WISER_SERVICES["SERVICE_GET_SCHEDULE"])
hass.services.async_remove(DOMAIN, WISER_SERVICES["SERVICE_SET_SCHEDULE"])
hass.services.async_remove(DOMAIN, WISER_SERVICES["SERVICE_COPY_SCHEDULE"])
Expand Down Expand Up @@ -516,43 +305,3 @@ async def async_update_device_registry(self):
model=self.wiserhub.system.model,
sw_version=self.wiserhub.system.firmware_version,
)

@callback
async def async_remove_orphaned_entries(self, entry_id, wiser_hub_id: str):
"""Remove orphaned Wiser entries from device registry"""
api = self._hass.data[DOMAIN][entry_id]["data"]

if api.wiserhub.system.name == wiser_hub_id:
_LOGGER.info(f"Removing orphaned devices for {wiser_hub_id}")

device_registry = dr.async_get(self._hass)
entity_registry = er.async_get(self._hass)

devices_to_be_removed = []

#Get list of all devices for integration
all_devices = [
entry
for entry in device_registry.devices.values()
if entry_id in entry.config_entries
]

# Don't remove the Gateway host entry
wiser_hub = device_registry.async_get_device(
connections={(CONNECTION_NETWORK_MAC, api.wiserhub.system.network.mac_address)},
identifiers={(DOMAIN, api.unique_id)},
)
devices_to_be_removed = [ device.id for device in all_devices if device.id != wiser_hub.id ]

# Remove devices that don't belong to any entity
for device_id in devices_to_be_removed:
if (
len(
async_entries_for_device(
entity_registry, device_id, include_disabled_entities=True
)
)
== 0
):
_LOGGER.info(f"Removed {device_id}")
device_registry.async_remove_device(device_id)
12 changes: 7 additions & 5 deletions custom_components/wiser/button.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@

from .const import (
ATTR_TIME_PERIOD,
DATA,
DEFAULT_BOOST_TEMP_TIME,
DOMAIN,
MANUFACTURER
MANUFACTURER,
WISER_SERVICES
)
from .climate import HVAC_MODE_HASS_TO_WISER
from .helpers import get_device_name, get_unique_id, get_identifier

import voluptuous as vol
from homeassistant.components.button import ButtonEntity
from homeassistant.components.climate.const import HVAC_MODE_AUTO
from homeassistant.core import callback
from homeassistant.helpers import entity_platform
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import dt as dt_util

Expand Down Expand Up @@ -42,7 +46,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):

async_add_entities(wiser_buttons, True)


class WiserButton(ButtonEntity):
def __init__(self, data, name = "Button"):
"""Initialize the sensor."""
Expand Down Expand Up @@ -141,7 +144,6 @@ async def async_press(self):
def icon(self):
return "mdi:water-plus"


class WiserCancelHotWaterOverridesButton(WiserButton):
def __init__(self, data):
super().__init__(data, "Cancel Hot Water Overrides")
Expand Down
Loading