diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index b8d46a4aec5..e78fda305da 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -1,13 +1,15 @@ """The sms component.""" import logging -import gammu # pylint: disable=import-error, no-member import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_DEVICE +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv -from .const import DOMAIN +from .const import DOMAIN, SMS_GATEWAY +from .gateway import create_sms_gateway _LOGGER = logging.getLogger(__name__) @@ -17,17 +19,38 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass, config): +async def async_setup(hass, config): """Configure Gammu state machine.""" - conf = config[DOMAIN] - device = conf.get(CONF_DEVICE) - gateway = gammu.StateMachine() # pylint: disable=no-member - try: - gateway.SetConfig(0, dict(Device=device, Connection="at")) - gateway.Init() - except gammu.GSMError as exc: # pylint: disable=no-member - _LOGGER.error("Failed to initialize, error %s", exc) - return False - else: - hass.data[DOMAIN] = gateway + hass.data.setdefault(DOMAIN, {}) + sms_config = config.get(DOMAIN, {}) + if not sms_config: return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=sms_config, + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Configure Gammu state machine.""" + + device = entry.data[CONF_DEVICE] + config = {"Device": device, "Connection": "at"} + gateway = await create_sms_gateway(config, hass) + if not gateway: + return False + hass.data[DOMAIN][SMS_GATEWAY] = gateway + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + + gateway = hass.data[DOMAIN].pop(SMS_GATEWAY) + await gateway.terminate_async() + return True diff --git a/homeassistant/components/sms/config_flow.py b/homeassistant/components/sms/config_flow.py new file mode 100644 index 00000000000..148360416a2 --- /dev/null +++ b/homeassistant/components/sms/config_flow.py @@ -0,0 +1,74 @@ +"""Config flow for SMS integration.""" +import logging + +import gammu # pylint: disable=import-error, no-member +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_DEVICE + +from .const import DOMAIN # pylint:disable=unused-import +from .gateway import create_sms_gateway + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_DEVICE): str}) + + +async def get_imei_from_config(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + device = data[CONF_DEVICE] + config = {"Device": device, "Connection": "at"} + gateway = await create_sms_gateway(config, hass) + if not gateway: + raise CannotConnect + try: + imei = await gateway.get_imei_async() + except gammu.GSMError: # pylint: disable=no-member + raise CannotConnect + finally: + await gateway.terminate_async() + + # Return info that you want to store in the config entry. + return imei + + +class SMSFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for SMS integration.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + errors = {} + if user_input is not None: + try: + imei = await get_imei_from_config(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if not errors: + await self.async_set_unique_id(imei) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=imei, data=user_input) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, user_input): + """Handle import.""" + return await self.async_step_user(user_input) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/sms/const.py b/homeassistant/components/sms/const.py index aff2b704e05..b73e7954fc1 100644 --- a/homeassistant/components/sms/const.py +++ b/homeassistant/components/sms/const.py @@ -1,3 +1,4 @@ """Constants for sms Component.""" DOMAIN = "sms" +SMS_GATEWAY = "SMS_GATEWAY" diff --git a/homeassistant/components/sms/gateway.py b/homeassistant/components/sms/gateway.py new file mode 100644 index 00000000000..8a75808f751 --- /dev/null +++ b/homeassistant/components/sms/gateway.py @@ -0,0 +1,42 @@ +"""The sms gateway to interact with a GSM modem.""" +import logging + +import gammu # pylint: disable=import-error, no-member +from gammu.asyncworker import ( # pylint: disable=import-error, no-member + GammuAsyncWorker, +) + +_LOGGER = logging.getLogger(__name__) + + +class Gateway: + """SMS gateway to interact with a GSM modem.""" + + def __init__(self, worker, hass): + """Initialize the sms gateway.""" + self._worker = worker + + async def send_sms_async(self, message): + """Send sms message via the worker.""" + return await self._worker.send_sms_async(message) + + async def get_imei_async(self): + """Get the IMEI of the device.""" + return await self._worker.get_imei_async() + + async def terminate_async(self): + """Terminate modem connection.""" + return await self._worker.terminate_async() + + +async def create_sms_gateway(config, hass): + """Create the sms gateway.""" + try: + worker = GammuAsyncWorker() + worker.configure(config) + await worker.init_async() + gateway = Gateway(worker, hass) + return gateway + except gammu.GSMError as exc: # pylint: disable=no-member + _LOGGER.error("Failed to initialize, error %s", exc) + return None diff --git a/homeassistant/components/sms/manifest.json b/homeassistant/components/sms/manifest.json index 8b65ac77e59..c3c7db2aa61 100644 --- a/homeassistant/components/sms/manifest.json +++ b/homeassistant/components/sms/manifest.json @@ -1,7 +1,8 @@ { "domain": "sms", "name": "SMS notifications via GSM-modem", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sms", - "requirements": ["python-gammu==2.12"], + "requirements": ["python-gammu==3.0"], "codeowners": ["@ocalvo"] } diff --git a/homeassistant/components/sms/notify.py b/homeassistant/components/sms/notify.py index f39ae8153bd..0b867b2e0a5 100644 --- a/homeassistant/components/sms/notify.py +++ b/homeassistant/components/sms/notify.py @@ -8,7 +8,7 @@ from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationSer from homeassistant.const import CONF_NAME, CONF_RECIPIENT import homeassistant.helpers.config_validation as cv -from .const import DOMAIN +from .const import DOMAIN, SMS_GATEWAY _LOGGER = logging.getLogger(__name__) @@ -19,8 +19,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the SMS notification service.""" - gateway = hass.data[DOMAIN] - number = config[CONF_RECIPIENT] + + if SMS_GATEWAY not in hass.data[DOMAIN]: + _LOGGER.error("SMS gateway not found, cannot initialize service") + return + + gateway = hass.data[DOMAIN][SMS_GATEWAY] + + if discovery_info is None: + number = config[CONF_RECIPIENT] + else: + number = discovery_info[CONF_RECIPIENT] + return SMSNotificationService(gateway, number) @@ -32,7 +42,7 @@ class SMSNotificationService(BaseNotificationService): self.gateway = gateway self.number = number - def send_message(self, message="", **kwargs): + async def send_message(self, message="", **kwargs): """Send SMS message.""" smsinfo = { "Class": -1, @@ -53,6 +63,6 @@ class SMSNotificationService(BaseNotificationService): encoded_message["Number"] = self.number try: # Actually send the message - self.gateway.SendSMS(encoded_message) + await self.gateway.send_sms_async(encoded_message) except gammu.GSMError as exc: # pylint: disable=no-member _LOGGER.error("Sending to %s failed: %s", self.number, exc) diff --git a/homeassistant/components/sms/strings.json b/homeassistant/components/sms/strings.json new file mode 100644 index 00000000000..6f92631e2e1 --- /dev/null +++ b/homeassistant/components/sms/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "title": "Connect to the modem", + "data": { "device": "Device" } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index cf950edb901..54678007eb7 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -139,6 +139,7 @@ FLOWS = [ "smappee", "smartthings", "smhi", + "sms", "solaredge", "solarlog", "soma", diff --git a/requirements_all.txt b/requirements_all.txt index ec06e60110d..a6ea39b314c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1694,7 +1694,7 @@ python-family-hub-local==0.0.2 python-forecastio==1.4.0 # homeassistant.components.sms -# python-gammu==2.12 +# python-gammu==3.0 # homeassistant.components.gc100 python-gc100==1.0.3a