diff --git a/custom_components/scheduler/__init__.py b/custom_components/scheduler/__init__.py index 80bc6e4..9942e9f 100755 --- a/custom_components/scheduler/__init__.py +++ b/custom_components/scheduler/__init__.py @@ -146,9 +146,16 @@ async def async_edit_schedule(self, schedule_id: str, data: dict): _LOGGER.debug("async_edit_schedule") if schedule_id not in self.hass.data[DOMAIN]["schedules"]: return + item = self.async_get_schedule(schedule_id) + + if "name" in data and item["name"] != data["name"]: + data["name"] = data["name"].strip() + elif "name" in data: + del data["name"] + entry = self.store.async_update_schedule(schedule_id, data) entity = self.hass.data[DOMAIN]["schedules"][schedule_id] - if "name" in data and data["name"]: + if "name" in data: entity_registry = await get_entity_registry(self.hass) entity_registry.async_remove(entity.entity_id) self._create_schedule_handler(entry) diff --git a/custom_components/scheduler/const.py b/custom_components/scheduler/const.py index f032ece..9d4702e 100755 --- a/custom_components/scheduler/const.py +++ b/custom_components/scheduler/const.py @@ -1,9 +1,6 @@ """Store constants.""" -import voluptuous as vol -from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.helpers import config_validation as cv -VERSION = "3.0.0b" +VERSION = "3.0.0" DOMAIN = "scheduler" ENTITY_ID_FORMAT = DOMAIN + ".{}" @@ -27,86 +24,3 @@ REPEAT_TYPE_REPEAT = "repeat" REPEAT_TYPE_SINGLE = "single" REPEAT_TYPE_PAUSE = "pause" - -CONDITION_SCHEMA = vol.Schema( - { - vol.Required("entity_id"): cv.entity_id, - vol.Required("value"): vol.Any(int, float, str), - vol.Optional("attribute"): cv.string, - vol.Required("match_type"): vol.In( - [MATCH_TYPE_EQUAL, MATCH_TYPE_UNEQUAL, MATCH_TYPE_BELOW, MATCH_TYPE_ABOVE] - ), - } -) - -ACTION_SCHEMA = vol.Schema( - { - vol.Optional("entity_id"): cv.entity_id, - vol.Optional("service"): cv.entity_id, - vol.Optional("service_data"): dict, - } -) - -TIMESLOT_SCHEMA = vol.Schema( - { - vol.Required("start"): cv.string, - vol.Optional("stop"): cv.string, - vol.Optional("conditions"): vol.All( - cv.ensure_list, vol.Length(min=1), [CONDITION_SCHEMA] - ), - vol.Optional("condition_type"): vol.In( - [ - CONDITION_TYPE_AND, - CONDITION_TYPE_OR, - ] - ), - vol.Required("actions"): vol.All( - cv.ensure_list, vol.Length(min=1), [ACTION_SCHEMA] - ), - } -) - -SCHEDULE_SCHEMA = vol.Schema( - { - vol.Required("weekdays"): vol.All( - cv.ensure_list, - vol.Unique(), - vol.Length(min=1), - [ - vol.In( - [ - "mon", - "tue", - "wed", - "thu", - "fri", - "sat", - "sun", - DAY_TYPE_WORKDAY, - DAY_TYPE_WEEKEND, - DAY_TYPE_DAILY, - ] - ) - ], - ), - vol.Required("timeslots"): vol.All( - cv.ensure_list, vol.Length(min=1), [TIMESLOT_SCHEMA] - ), - vol.Required("repeat_type"): vol.In( - [ - REPEAT_TYPE_REPEAT, - REPEAT_TYPE_SINGLE, - REPEAT_TYPE_PAUSE, - ] - ), - vol.Optional("name"): cv.string, - } -) - -SERVICE_RUN_ACTION = "run_action" -RUN_ACTION_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Optional("time"): cv.time - } -) diff --git a/custom_components/scheduler/helpers.py b/custom_components/scheduler/helpers.py index c00d50e..8a0147c 100755 --- a/custom_components/scheduler/helpers.py +++ b/custom_components/scheduler/helpers.py @@ -2,6 +2,7 @@ import logging import math import re +import voluptuous as vol import homeassistant.util.dt as dt_util from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET @@ -252,3 +253,21 @@ def has_overlapping_timeslot( return i return None + + +def validate_time(time): + res = OffsetTimePattern.match(time) + if not res: + if dt_util.parse_time(time): + return time + else: + raise vol.Invalid("Invalid time entered: {}".format(time)) + else: + if res.group(1) not in [SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET]: + raise vol.Invalid("Invalid time entered: {}".format(time)) + elif res.group(2) not in ['+', '-']: + raise vol.Invalid("Invalid time entered: {}".format(time)) + elif not dt_util.parse_time(res.group(3)): + raise vol.Invalid("Invalid time entered: {}".format(time)) + else: + return time diff --git a/custom_components/scheduler/switch.py b/custom_components/scheduler/switch.py index c211678..7276cda 100755 --- a/custom_components/scheduler/switch.py +++ b/custom_components/scheduler/switch.py @@ -2,15 +2,17 @@ import copy import datetime import logging +import voluptuous as vol from homeassistant.components.switch import DOMAIN as PLATFORM -from homeassistant.helpers import entity_platform +from homeassistant.helpers import (entity_platform, config_validation as cv) from homeassistant.const import ( STATE_ALARM_TRIGGERED as STATE_TRIGGERED, STATE_OFF, STATE_ON, SUN_EVENT_SUNRISE, - SUN_EVENT_SUNSET + SUN_EVENT_SUNSET, + ATTR_ENTITY_ID, ) from homeassistant.core import callback from homeassistant.helpers.device_registry import async_entries_for_config_entry @@ -38,8 +40,6 @@ REPEAT_TYPE_PAUSE, REPEAT_TYPE_SINGLE, VERSION, - SERVICE_RUN_ACTION, - RUN_ACTION_SCHEMA, ) from .helpers import calculate_next_start_time, has_overlapping_timeslot from .migrate import migrate_old_entity @@ -49,6 +49,15 @@ _LOGGER = logging.getLogger(__name__) +SERVICE_RUN_ACTION = "run_action" +RUN_ACTION_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional("time"): cv.time + } +) + + def entity_exists_in_hass(hass, entity_id): """Check that an entity exists.""" return hass.states.get(entity_id) is not None @@ -98,7 +107,7 @@ def async_add_entity(schedule: ScheduleEntry): schedule_id = schedule.schedule_id name = schedule.name - if name: + if name and len(slugify(name)): entity_id = "{}.schedule_{}".format(PLATFORM, slugify(name)) else: entity_id = "{}.schedule_{}".format(PLATFORM, schedule_id) diff --git a/custom_components/scheduler/websockets.py b/custom_components/scheduler/websockets.py index 6677d49..7e1f8fd 100755 --- a/custom_components/scheduler/websockets.py +++ b/custom_components/scheduler/websockets.py @@ -1,18 +1,110 @@ import logging import voluptuous as vol +from homeassistant.helpers import config_validation as cv +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.components import websocket_api from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv - -from .const import DOMAIN, SCHEDULE_SCHEMA +from .helpers import validate_time + +from .const import ( + DOMAIN, + MATCH_TYPE_EQUAL, + MATCH_TYPE_UNEQUAL, + MATCH_TYPE_BELOW, + MATCH_TYPE_ABOVE, + CONDITION_TYPE_AND, + CONDITION_TYPE_OR, + DAY_TYPE_WORKDAY, + DAY_TYPE_WEEKEND, + DAY_TYPE_DAILY, + REPEAT_TYPE_REPEAT, + REPEAT_TYPE_SINGLE, + REPEAT_TYPE_PAUSE, +) _LOGGER = logging.getLogger(__name__) EVENT = "schedules_updated" +CONDITION_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required("value"): vol.Any(int, float, str), + vol.Optional("attribute"): cv.string, + vol.Required("match_type"): vol.In( + [MATCH_TYPE_EQUAL, MATCH_TYPE_UNEQUAL, MATCH_TYPE_BELOW, MATCH_TYPE_ABOVE] + ), + } +) + +ACTION_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_ENTITY_ID): cv.entity_id, + vol.Optional("service"): cv.entity_id, + vol.Optional("service_data"): dict, + } +) + +TIMESLOT_SCHEMA = vol.Schema( + { + vol.Required("start"): validate_time, + vol.Optional("stop"): validate_time, + vol.Optional("conditions"): vol.All( + cv.ensure_list, vol.Length(min=1), [CONDITION_SCHEMA] + ), + vol.Optional("condition_type"): vol.In( + [ + CONDITION_TYPE_AND, + CONDITION_TYPE_OR, + ] + ), + vol.Required("actions"): vol.All( + cv.ensure_list, vol.Length(min=1), [ACTION_SCHEMA] + ), + } +) + +SCHEDULE_SCHEMA = vol.Schema( + { + vol.Required("weekdays"): vol.All( + cv.ensure_list, + vol.Unique(), + vol.Length(min=1), + [ + vol.In( + [ + "mon", + "tue", + "wed", + "thu", + "fri", + "sat", + "sun", + DAY_TYPE_WORKDAY, + DAY_TYPE_WEEKEND, + DAY_TYPE_DAILY, + ] + ) + ], + ), + vol.Required("timeslots"): vol.All( + cv.ensure_list, vol.Length(min=1), [TIMESLOT_SCHEMA] + ), + vol.Required("repeat_type"): vol.In( + [ + REPEAT_TYPE_REPEAT, + REPEAT_TYPE_SINGLE, + REPEAT_TYPE_PAUSE, + ] + ), + vol.Optional("name"): cv.string, + } +) + + class SchedulesAddView(HomeAssistantView): """Login to Home Assistant cloud.""" @@ -58,7 +150,7 @@ async def post(self, request, data): """Handle config update request.""" hass = request.app["hass"] coordinator = hass.data[DOMAIN]["coordinator"] - coordinator.async_delete_schedule(data["schedule_id"]) + await coordinator.async_delete_schedule(data["schedule_id"]) return self.json({"success": True})