From 6088f5eef5f485f141c52bf4a1c92fe89aa25167 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 29 Oct 2025 23:21:02 +0100 Subject: [PATCH] Remove sms integration (#155460) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/ci.yaml | 6 +- .github/workflows/wheels.yml | 2 +- CODEOWNERS | 2 - Dockerfile.dev | 1 - homeassistant/components/sms/__init__.py | 126 ------------ homeassistant/components/sms/config_flow.py | 87 -------- homeassistant/components/sms/const.py | 35 ---- homeassistant/components/sms/coordinator.py | 59 ------ homeassistant/components/sms/gateway.py | 211 -------------------- homeassistant/components/sms/icons.json | 12 -- homeassistant/components/sms/manifest.json | 10 - homeassistant/components/sms/notify.py | 85 -------- homeassistant/components/sms/sensor.py | 120 ----------- homeassistant/components/sms/strings.json | 46 ----- homeassistant/generated/config_flows.py | 1 - homeassistant/generated/integrations.json | 6 - requirements_all.txt | 3 - requirements_test_all.txt | 3 - script/gen_requirements_all.py | 4 +- tests/components/sms/__init__.py | 1 - tests/components/sms/const.py | 143 ------------- tests/components/sms/test_gateway.py | 52 ----- tests/components/sms/test_init.py | 59 ------ 23 files changed, 3 insertions(+), 1071 deletions(-) delete mode 100644 homeassistant/components/sms/__init__.py delete mode 100644 homeassistant/components/sms/config_flow.py delete mode 100644 homeassistant/components/sms/const.py delete mode 100644 homeassistant/components/sms/coordinator.py delete mode 100644 homeassistant/components/sms/gateway.py delete mode 100644 homeassistant/components/sms/icons.json delete mode 100644 homeassistant/components/sms/manifest.json delete mode 100644 homeassistant/components/sms/notify.py delete mode 100644 homeassistant/components/sms/sensor.py delete mode 100644 homeassistant/components/sms/strings.json delete mode 100644 tests/components/sms/__init__.py delete mode 100644 tests/components/sms/const.py delete mode 100644 tests/components/sms/test_gateway.py delete mode 100644 tests/components/sms/test_init.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cdebf3007a7..619cd9beb31 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -502,7 +502,6 @@ jobs: libavfilter-dev \ libavformat-dev \ libavutil-dev \ - libgammu-dev \ libswresample-dev \ libswscale-dev \ libudev-dev @@ -801,8 +800,7 @@ jobs: -o Dir::State::Lists=${{ env.APT_LIST_CACHE_DIR }} \ bluez \ ffmpeg \ - libturbojpeg \ - libgammu-dev + libturbojpeg - *checkout - *setup-python-default - *cache-restore-python-default @@ -853,7 +851,6 @@ jobs: bluez \ ffmpeg \ libturbojpeg \ - libgammu-dev \ libxml2-utils - *checkout - *setup-python-matrix @@ -1233,7 +1230,6 @@ jobs: bluez \ ffmpeg \ libturbojpeg \ - libgammu-dev \ libxml2-utils - *checkout - *setup-python-matrix diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 91539fa3bb0..1c1c12126ed 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -228,7 +228,7 @@ jobs: arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-ng-dev" + apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-ng-dev" skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" diff --git a/CODEOWNERS b/CODEOWNERS index 218d4a47def..d61dcf3ade2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1479,8 +1479,6 @@ build.json @home-assistant/supervisor /tests/components/smhi/ @gjohansson-ST /homeassistant/components/smlight/ @tl-sl /tests/components/smlight/ @tl-sl -/homeassistant/components/sms/ @ocalvo -/tests/components/sms/ @ocalvo /homeassistant/components/snapcast/ @luar123 /tests/components/snapcast/ @luar123 /homeassistant/components/snmp/ @nmaggioni diff --git a/Dockerfile.dev b/Dockerfile.dev index e57fae2a005..1e21b8c815c 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -13,7 +13,6 @@ RUN \ libavcodec-dev \ libavdevice-dev \ libavutil-dev \ - libgammu-dev \ libswscale-dev \ libswresample-dev \ libavfilter-dev \ diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py deleted file mode 100644 index 78f7899a571..00000000000 --- a/homeassistant/components/sms/__init__.py +++ /dev/null @@ -1,126 +0,0 @@ -"""The sms component.""" - -import logging - -import voluptuous as vol - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_DEVICE, CONF_NAME, Platform -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers.issue_registry import ( - IssueSeverity, - async_create_issue, - async_delete_issue, -) -from homeassistant.helpers.typing import ConfigType - -from .const import ( - CONF_BAUD_SPEED, - DEFAULT_BAUD_SPEED, - DOMAIN, - GATEWAY, - HASS_CONFIG, - NETWORK_COORDINATOR, - SIGNAL_COORDINATOR, - SMS_GATEWAY, -) -from .coordinator import NetworkCoordinator, SignalCoordinator -from .gateway import create_sms_gateway - -_LOGGER = logging.getLogger(__name__) - -PLATFORMS = [Platform.SENSOR] - -SMS_CONFIG_SCHEMA = {vol.Required(CONF_DEVICE): cv.isdevice} - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - vol.All( - cv.deprecated(CONF_DEVICE), - SMS_CONFIG_SCHEMA, - ), - ) - }, - extra=vol.ALLOW_EXTRA, -) -DEPRECATED_ISSUE_ID = f"deprecated_system_packages_config_flow_integration_{DOMAIN}" - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Configure Gammu state machine.""" - hass.data.setdefault(DOMAIN, {}) - hass.data[HASS_CONFIG] = config - return True - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Configure Gammu state machine.""" - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - DEPRECATED_ISSUE_ID, - breaks_in_ha_version="2025.12.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_system_packages_config_flow_integration", - translation_placeholders={ - "integration_title": "SMS notifications via GSM-modem", - }, - ) - - device = entry.data[CONF_DEVICE] - connection_mode = "at" - baud_speed = entry.data.get(CONF_BAUD_SPEED, DEFAULT_BAUD_SPEED) - if baud_speed != DEFAULT_BAUD_SPEED: - connection_mode += baud_speed - config = {"Device": device, "Connection": connection_mode} - _LOGGER.debug("Connecting mode:%s", connection_mode) - gateway = await create_sms_gateway(config, hass) - if not gateway: - raise ConfigEntryNotReady(f"Cannot find device {device}") - - signal_coordinator = SignalCoordinator(hass, entry, gateway) - network_coordinator = NetworkCoordinator(hass, entry, gateway) - - # Fetch initial data so we have data when entities subscribe - await signal_coordinator.async_config_entry_first_refresh() - await network_coordinator.async_config_entry_first_refresh() - - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][SMS_GATEWAY] = { - SIGNAL_COORDINATOR: signal_coordinator, - NETWORK_COORDINATOR: network_coordinator, - GATEWAY: gateway, - } - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - - # set up notify platform, no entry support for notify component yet, - # have to use discovery to load platform. - hass.async_create_task( - discovery.async_load_platform( - hass, - Platform.NOTIFY, - DOMAIN, - {CONF_NAME: DOMAIN}, - hass.data[HASS_CONFIG], - ) - ) - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - gateway = hass.data[DOMAIN].pop(SMS_GATEWAY)[GATEWAY] - await gateway.terminate_async() - - if not hass.config_entries.async_loaded_entries(DOMAIN): - async_delete_issue(hass, HOMEASSISTANT_DOMAIN, DEPRECATED_ISSUE_ID) - - return unload_ok diff --git a/homeassistant/components/sms/config_flow.py b/homeassistant/components/sms/config_flow.py deleted file mode 100644 index d2188a94632..00000000000 --- a/homeassistant/components/sms/config_flow.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Config flow for SMS integration.""" - -import logging -from typing import Any - -import gammu -import voluptuous as vol - -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_DEVICE -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import selector - -from .const import CONF_BAUD_SPEED, DEFAULT_BAUD_SPEED, DEFAULT_BAUD_SPEEDS, DOMAIN -from .gateway import create_sms_gateway - -_LOGGER = logging.getLogger(__name__) - -DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_DEVICE): str, - vol.Optional(CONF_BAUD_SPEED, default=DEFAULT_BAUD_SPEED): selector.selector( - {"select": {"options": DEFAULT_BAUD_SPEEDS}} - ), - } -) - - -async def get_imei_from_config(hass: HomeAssistant, data: dict[str, Any]) -> str: - """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] - connection_mode = "at" - baud_speed = data.get(CONF_BAUD_SPEED, DEFAULT_BAUD_SPEED) - if baud_speed != DEFAULT_BAUD_SPEED: - connection_mode += baud_speed - config = {"Device": device, "Connection": connection_mode} - gateway = await create_sms_gateway(config, hass) - if not gateway: - raise CannotConnect - try: - imei = await gateway.get_imei_async() - except gammu.GSMError as err: - raise CannotConnect from err - finally: - await gateway.terminate_async() - - # Return info that you want to store in the config entry. - return imei - - -class SMSFlowHandler(ConfigFlow, domain=DOMAIN): - """Handle a config flow for SMS integration.""" - - VERSION = 1 - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """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: - _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 - ) - - -class CannotConnect(HomeAssistantError): - """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/sms/const.py b/homeassistant/components/sms/const.py deleted file mode 100644 index 5c7a2ce86a4..00000000000 --- a/homeassistant/components/sms/const.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Constants for sms Component.""" - -DOMAIN = "sms" -SMS_GATEWAY = "SMS_GATEWAY" -HASS_CONFIG = "sms_hass_config" -SMS_STATE_UNREAD = "UnRead" -SIGNAL_COORDINATOR = "signal_coordinator" -NETWORK_COORDINATOR = "network_coordinator" -GATEWAY = "gateway" -DEFAULT_SCAN_INTERVAL = 30 -CONF_BAUD_SPEED = "baud_speed" -CONF_UNICODE = "unicode" -DEFAULT_BAUD_SPEED = "0" -DEFAULT_BAUD_SPEEDS = [ - {"value": DEFAULT_BAUD_SPEED, "label": "Auto"}, - {"value": "50", "label": "50"}, - {"value": "75", "label": "75"}, - {"value": "110", "label": "110"}, - {"value": "134", "label": "134"}, - {"value": "150", "label": "150"}, - {"value": "200", "label": "200"}, - {"value": "300", "label": "300"}, - {"value": "600", "label": "600"}, - {"value": "1200", "label": "1200"}, - {"value": "1800", "label": "1800"}, - {"value": "2400", "label": "2400"}, - {"value": "4800", "label": "4800"}, - {"value": "9600", "label": "9600"}, - {"value": "19200", "label": "19200"}, - {"value": "28800", "label": "28800"}, - {"value": "38400", "label": "38400"}, - {"value": "57600", "label": "57600"}, - {"value": "76800", "label": "76800"}, - {"value": "115200", "label": "115200"}, -] diff --git a/homeassistant/components/sms/coordinator.py b/homeassistant/components/sms/coordinator.py deleted file mode 100644 index 858fc303805..00000000000 --- a/homeassistant/components/sms/coordinator.py +++ /dev/null @@ -1,59 +0,0 @@ -"""DataUpdateCoordinators for the sms integration.""" - -import asyncio -from datetime import timedelta -import logging - -import gammu - -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed - -from .const import DEFAULT_SCAN_INTERVAL - -_LOGGER = logging.getLogger(__name__) - - -class SignalCoordinator(DataUpdateCoordinator): - """Signal strength coordinator.""" - - def __init__(self, hass, entry, gateway): - """Initialize signal strength coordinator.""" - super().__init__( - hass, - _LOGGER, - name="Device signal state", - update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), - config_entry=entry, - ) - self._gateway = gateway - - async def _async_update_data(self): - """Fetch device signal quality.""" - try: - async with asyncio.timeout(10): - return await self._gateway.get_signal_quality_async() - except gammu.GSMError as exc: - raise UpdateFailed(f"Error communicating with device: {exc}") from exc - - -class NetworkCoordinator(DataUpdateCoordinator): - """Network info coordinator.""" - - def __init__(self, hass, entry, gateway): - """Initialize network info coordinator.""" - super().__init__( - hass, - _LOGGER, - name="Device network state", - update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), - config_entry=entry, - ) - self._gateway = gateway - - async def _async_update_data(self): - """Fetch device network info.""" - try: - async with asyncio.timeout(10): - return await self._gateway.get_network_info_async() - except gammu.GSMError as exc: - raise UpdateFailed(f"Error communicating with device: {exc}") from exc diff --git a/homeassistant/components/sms/gateway.py b/homeassistant/components/sms/gateway.py deleted file mode 100644 index a11996e3dfc..00000000000 --- a/homeassistant/components/sms/gateway.py +++ /dev/null @@ -1,211 +0,0 @@ -"""The sms gateway to interact with a GSM modem.""" - -import logging - -import gammu -from gammu.asyncworker import GammuAsyncWorker - -from homeassistant.core import callback - -from .const import DOMAIN, SMS_STATE_UNREAD - -_LOGGER = logging.getLogger(__name__) - - -class Gateway: - """SMS gateway to interact with a GSM modem.""" - - def __init__(self, config, hass): - """Initialize the sms gateway.""" - _LOGGER.debug("Init with connection mode:%s", config["Connection"]) - self._worker = GammuAsyncWorker(self.sms_pull) - self._worker.configure(config) - self._hass = hass - self._first_pull = True - self.manufacturer = None - self.model = None - self.firmware = None - - async def init_async(self): - """Initialize the sms gateway asynchronously. This method is also called in config flow to verify connection.""" - await self._worker.init_async() - self.manufacturer = await self.get_manufacturer_async() - self.model = await self.get_model_async() - self.firmware = await self.get_firmware_async() - - def sms_pull(self, state_machine): - """Pull device. - - @param state_machine: state machine - @type state_machine: gammu.StateMachine - """ - state_machine.ReadDevice() - - _LOGGER.debug("Pulling modem") - self.sms_read_messages(state_machine, self._first_pull) - self._first_pull = False - - def sms_read_messages(self, state_machine, force=False): - """Read all received SMS messages. - - @param state_machine: state machine which invoked action - @type state_machine: gammu.StateMachine - """ - entries = self.get_and_delete_all_sms(state_machine, force) - _LOGGER.debug("SMS entries:%s", entries) - data = [] - - for entry in entries: - decoded_entry = gammu.DecodeSMS(entry) - message = entry[0] - _LOGGER.debug("Processing sms:%s,decoded:%s", message, decoded_entry) - sms_state = message["State"] - _LOGGER.debug("SMS state:%s", sms_state) - if sms_state == SMS_STATE_UNREAD: - if decoded_entry is None: - text = message["Text"] - else: - text = "" - for inner_entry in decoded_entry["Entries"]: - if inner_entry["Buffer"] is not None: - text += inner_entry["Buffer"] - - event_data = { - "phone": message["Number"], - "date": str(message["DateTime"]), - "message": text, - } - - _LOGGER.debug("Append event data:%s", event_data) - data.append(event_data) - - self._hass.add_job(self._notify_incoming_sms, data) - - def get_and_delete_all_sms(self, state_machine, force=False): - """Read and delete all SMS in the modem.""" - # Read SMS memory status ... - memory = state_machine.GetSMSStatus() - # ... and calculate number of messages - remaining = memory["SIMUsed"] + memory["PhoneUsed"] - start_remaining = remaining - # Get all sms - start = True - entries = [] - all_parts = -1 - _LOGGER.debug("Start remaining:%i", start_remaining) - - try: - while remaining > 0: - if start: - entry = state_machine.GetNextSMS(Folder=0, Start=True) - all_parts = entry[0]["UDH"]["AllParts"] - part_number = entry[0]["UDH"]["PartNumber"] - part_is_missing = all_parts > start_remaining - _LOGGER.debug("All parts:%i", all_parts) - _LOGGER.debug("Part Number:%i", part_number) - _LOGGER.debug("Remaining:%i", remaining) - _LOGGER.debug("Start is_part_missing:%s", part_is_missing) - start = False - else: - entry = state_machine.GetNextSMS( - Folder=0, Location=entry[0]["Location"] - ) - - if part_is_missing and not force: - _LOGGER.debug("Not all parts have arrived") - break - - remaining = remaining - 1 - entries.append(entry) - - # delete retrieved sms - _LOGGER.debug("Deleting message") - try: - state_machine.DeleteSMS(Folder=0, Location=entry[0]["Location"]) - except gammu.ERR_MEMORY_NOT_AVAILABLE: - _LOGGER.error("Error deleting SMS, memory not available") - - except gammu.ERR_EMPTY: - # error is raised if memory is empty (this induces wrong reported - # memory status) - _LOGGER.warning("Failed to read messages!") - - # Link all SMS when there are concatenated messages - return gammu.LinkSMS(entries) - - @callback - def _notify_incoming_sms(self, messages): - """Notify hass when an incoming SMS message is received.""" - for message in messages: - event_data = { - "phone": message["phone"], - "date": message["date"], - "text": message["message"], - } - self._hass.bus.async_fire(f"{DOMAIN}.incoming_sms", event_data) - - 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 get_signal_quality_async(self): - """Get the current signal level of the modem.""" - return await self._worker.get_signal_quality_async() - - async def get_network_info_async(self): - """Get the current network info of the modem.""" - network_info = await self._worker.get_network_info_async() - # Looks like there is a bug and it's empty for any modem https://github.com/gammu/python-gammu/issues/31, so try workaround - if not network_info["NetworkName"]: - network_info["NetworkName"] = gammu.GSMNetworks.get( - network_info["NetworkCode"] - ) - return network_info - - async def get_manufacturer_async(self): - """Get the manufacturer of the modem.""" - return await self._worker.get_manufacturer_async() - - async def get_model_async(self): - """Get the model of the modem.""" - model = await self._worker.get_model_async() - if not model or not model[0]: - return None - display = model[0] # Identification model - if model[1]: # Real model - display = f"{display} ({model[1]})" - return display - - async def get_firmware_async(self): - """Get the firmware information of the modem.""" - firmware = await self._worker.get_firmware_async() - if not firmware or not firmware[0]: - return None - display = firmware[0] # Version - if firmware[1]: # Date - display = f"{display} ({firmware[1]})" - return display - - 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: - gateway = Gateway(config, hass) - try: - await gateway.init_async() - except gammu.GSMError as exc: - _LOGGER.error("Failed to initialize, error %s", exc) - await gateway.terminate_async() - return None - except gammu.GSMError as exc: - _LOGGER.error("Failed to create async worker, error %s", exc) - return None - return gateway diff --git a/homeassistant/components/sms/icons.json b/homeassistant/components/sms/icons.json deleted file mode 100644 index 6ed4cd63c67..00000000000 --- a/homeassistant/components/sms/icons.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "entity": { - "sensor": { - "cid": { - "default": "mdi:radio-tower" - }, - "signal_percent": { - "default": "mdi:signal-cellular-3" - } - } - } -} diff --git a/homeassistant/components/sms/manifest.json b/homeassistant/components/sms/manifest.json deleted file mode 100644 index 562819c2b4e..00000000000 --- a/homeassistant/components/sms/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "sms", - "name": "SMS notifications via GSM-modem", - "codeowners": ["@ocalvo"], - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/sms", - "iot_class": "local_polling", - "loggers": ["gammu"], - "requirements": ["python-gammu==3.2.4"] -} diff --git a/homeassistant/components/sms/notify.py b/homeassistant/components/sms/notify.py deleted file mode 100644 index 3374681c0f3..00000000000 --- a/homeassistant/components/sms/notify.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Support for SMS notification services.""" - -from __future__ import annotations - -import logging - -import gammu - -from homeassistant.components.notify import ATTR_DATA, BaseNotificationService -from homeassistant.const import CONF_TARGET -from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from .const import CONF_UNICODE, DOMAIN, GATEWAY, SMS_GATEWAY - -_LOGGER = logging.getLogger(__name__) - - -async def async_get_service( - hass: HomeAssistant, - config: ConfigType, - discovery_info: DiscoveryInfoType | None = None, -) -> SMSNotificationService | None: - """Get the SMS notification service.""" - - if discovery_info is None: - return None - - return SMSNotificationService(hass) - - -class SMSNotificationService(BaseNotificationService): - """Implement the notification service for SMS.""" - - def __init__(self, hass): - """Initialize the service.""" - - self.hass = hass - - async def async_send_message(self, message="", **kwargs): - """Send SMS message.""" - - if SMS_GATEWAY not in self.hass.data[DOMAIN]: - _LOGGER.error("SMS gateway not found, cannot send message") - return - - gateway = self.hass.data[DOMAIN][SMS_GATEWAY][GATEWAY] - - targets = kwargs.get(CONF_TARGET) - if targets is None: - _LOGGER.error("No target number specified, cannot send message") - return - - extended_data = kwargs.get(ATTR_DATA) - _LOGGER.debug("Extended data:%s", extended_data) - - if extended_data is None: - is_unicode = True - else: - is_unicode = extended_data.get(CONF_UNICODE, True) - - smsinfo = { - "Class": -1, - "Unicode": is_unicode, - "Entries": [{"ID": "ConcatenatedTextLong", "Buffer": message}], - } - try: - # Encode messages - encoded = gammu.EncodeSMS(smsinfo) - except gammu.GSMError as exc: - _LOGGER.error("Encoding message %s failed: %s", message, exc) - return - - # Send messages - for encoded_message in encoded: - # Fill in numbers - encoded_message["SMSC"] = {"Location": 1} - - for target in targets: - encoded_message["Number"] = target - try: - # Actually send the message - await gateway.send_sms_async(encoded_message) - except gammu.GSMError as exc: - _LOGGER.error("Sending to %s failed: %s", target, exc) diff --git a/homeassistant/components/sms/sensor.py b/homeassistant/components/sms/sensor.py deleted file mode 100644 index 46ee754a1f1..00000000000 --- a/homeassistant/components/sms/sensor.py +++ /dev/null @@ -1,120 +0,0 @@ -"""Support for SMS dongle sensor.""" - -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS, EntityCategory -from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from .const import DOMAIN, GATEWAY, NETWORK_COORDINATOR, SIGNAL_COORDINATOR, SMS_GATEWAY - -SIGNAL_SENSORS = ( - SensorEntityDescription( - key="SignalStrength", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="SignalPercent", - translation_key="signal_percent", - native_unit_of_measurement=PERCENTAGE, - entity_registry_enabled_default=True, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="BitErrorRate", - translation_key="bit_error_rate", - entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=PERCENTAGE, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), -) - -NETWORK_SENSORS = ( - SensorEntityDescription( - key="NetworkName", - translation_key="network_name", - entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="State", - translation_key="state", - entity_registry_enabled_default=True, - ), - SensorEntityDescription( - key="NetworkCode", - translation_key="network_code", - entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="CID", - translation_key="cid", - entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="LAC", - translation_key="lac", - entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, - ), -) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, -) -> None: - """Set up all device sensors.""" - sms_data = hass.data[DOMAIN][SMS_GATEWAY] - signal_coordinator = sms_data[SIGNAL_COORDINATOR] - network_coordinator = sms_data[NETWORK_COORDINATOR] - gateway = sms_data[GATEWAY] - unique_id = str(await gateway.get_imei_async()) - entities = [ - DeviceSensor(signal_coordinator, description, unique_id, gateway) - for description in SIGNAL_SENSORS - ] - entities.extend( - DeviceSensor(network_coordinator, description, unique_id, gateway) - for description in NETWORK_SENSORS - ) - async_add_entities(entities, True) - - -class DeviceSensor(CoordinatorEntity, SensorEntity): - """Implementation of a device sensor.""" - - _attr_has_entity_name = True - - def __init__(self, coordinator, description, unique_id, gateway): - """Initialize the device sensor.""" - super().__init__(coordinator) - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, unique_id)}, - name="SMS Gateway", - manufacturer=gateway.manufacturer, - model=gateway.model, - sw_version=gateway.firmware, - ) - self._attr_unique_id = f"{unique_id}_{description.key}" - self.entity_description = description - - @property - def native_value(self): - """Return the state of the device.""" - return self.coordinator.data.get(self.entity_description.key) diff --git a/homeassistant/components/sms/strings.json b/homeassistant/components/sms/strings.json deleted file mode 100644 index 62547603ba2..00000000000 --- a/homeassistant/components/sms/strings.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "step": { - "user": { - "data": { - "baud_speed": "Baud Speed", - "device": "[%key:common::config_flow::data::device%]" - }, - "title": "Connect to the modem" - } - } - }, - "entity": { - "sensor": { - "bit_error_rate": { - "name": "Bit error rate" - }, - "cid": { - "name": "Cell ID" - }, - "lac": { - "name": "Local area code" - }, - "network_code": { - "name": "GSM network code" - }, - "network_name": { - "name": "Network name" - }, - "signal_percent": { - "name": "Signal percent" - }, - "state": { - "name": "Network status" - } - } - } -} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 294549c9d07..1474c4ca3fb 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -612,7 +612,6 @@ FLOWS = { "smarty", "smhi", "smlight", - "sms", "snapcast", "snoo", "snooz", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 68f1ee27ab5..2f862e7b91d 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6217,12 +6217,6 @@ "config_flow": true, "iot_class": "local_push" }, - "sms": { - "name": "SMS notifications via GSM-modem", - "integration_type": "hub", - "config_flow": true, - "iot_class": "local_polling" - }, "smtp": { "name": "SMTP", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 26c84caf187..e8013bad6df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2477,9 +2477,6 @@ python-family-hub-local==0.0.2 # homeassistant.components.fully_kiosk python-fullykiosk==0.0.14 -# homeassistant.components.sms -# python-gammu==3.2.4 - # homeassistant.components.gc100 python-gc100==1.0.3a0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 312f4845a23..51fb4f38c8c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2062,9 +2062,6 @@ python-ecobee-api==0.2.20 # homeassistant.components.fully_kiosk python-fullykiosk==0.0.14 -# homeassistant.components.sms -# python-gammu==3.2.4 - # homeassistant.components.google_drive python-google-drive-api==0.1.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index e22c57b5256..0371818c271 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -31,7 +31,6 @@ EXCLUDED_REQUIREMENTS_ALL = { "face-recognition", "pybluez", "pycups", - "python-gammu", "python-lirc", "pyuserinput", } @@ -41,7 +40,6 @@ EXCLUDED_REQUIREMENTS_ALL = { INCLUDED_REQUIREMENTS_WHEELS = { "evdev", "pycups", - "python-gammu", "pyuserinput", } @@ -55,7 +53,7 @@ INCLUDED_REQUIREMENTS_WHEELS = { OVERRIDDEN_REQUIREMENTS_ACTIONS = { "pytest": { "exclude": set(), - "include": {"python-gammu"}, + "include": set(), "markers": {}, }, "wheels_aarch64": { diff --git a/tests/components/sms/__init__.py b/tests/components/sms/__init__.py deleted file mode 100644 index 09b4b0941fb..00000000000 --- a/tests/components/sms/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for SMS integration.""" diff --git a/tests/components/sms/const.py b/tests/components/sms/const.py deleted file mode 100644 index ae875e6d58e..00000000000 --- a/tests/components/sms/const.py +++ /dev/null @@ -1,143 +0,0 @@ -"""Constants for tests of the SMS component.""" - -import datetime - -SMS_STATUS_SINGLE = { - "SIMUnRead": 0, - "SIMUsed": 1, - "SIMSize": 30, - "PhoneUnRead": 0, - "PhoneUsed": 0, - "PhoneSize": 50, - "TemplatesUsed": 0, -} - -NEXT_SMS_SINGLE = [ - { - "SMSC": { - "Location": 0, - "Name": "", - "Format": "Text", - "Validity": "NA", - "Number": "+358444111111", - "DefaultNumber": "", - }, - "UDH": { - "Type": "NoUDH", - "Text": b"", - "ID8bit": 0, - "ID16bit": 0, - "PartNumber": -1, - "AllParts": 0, - }, - "Folder": 1, - "InboxFolder": 1, - "Memory": "SM", - "Location": 1, - "Name": "", - "Number": "+358444222222", - "Text": "Short message", - "Type": "Deliver", - "Coding": "Default_No_Compression", - "DateTime": datetime.datetime(2024, 3, 23, 20, 15, 37), - "SMSCDateTime": datetime.datetime(2024, 3, 23, 20, 15, 41), - "DeliveryStatus": 0, - "ReplyViaSameSMSC": 0, - "State": "UnRead", - "Class": -1, - "MessageReference": 0, - "ReplaceMessage": 0, - "RejectDuplicates": 0, - "Length": 7, - } -] - -SMS_STATUS_MULTIPLE = { - "SIMUnRead": 0, - "SIMUsed": 2, - "SIMSize": 30, - "PhoneUnRead": 0, - "PhoneUsed": 0, - "PhoneSize": 50, - "TemplatesUsed": 0, -} - -NEXT_SMS_MULTIPLE_1 = [ - { - "SMSC": { - "Location": 0, - "Name": "", - "Format": "Text", - "Validity": "NA", - "Number": "+358444111111", - "DefaultNumber": "", - }, - "UDH": { - "Type": "ConcatenatedMessages", - "Text": b"\x05\x00\x03\x00\x02\x01", - "ID8bit": 0, - "ID16bit": -1, - "PartNumber": 1, - "AllParts": 2, - }, - "Folder": 1, - "InboxFolder": 1, - "Memory": "SM", - "Location": 1, - "Name": "", - "Number": "+358444222222", - "Text": "Longer test again: 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123", - "Type": "Deliver", - "Coding": "Default_No_Compression", - "DateTime": datetime.datetime(2024, 3, 25, 19, 53, 56), - "SMSCDateTime": datetime.datetime(2024, 3, 25, 19, 54, 6), - "DeliveryStatus": 0, - "ReplyViaSameSMSC": 0, - "State": "UnRead", - "Class": -1, - "MessageReference": 0, - "ReplaceMessage": 0, - "RejectDuplicates": 0, - "Length": 153, - } -] - -NEXT_SMS_MULTIPLE_2 = [ - { - "SMSC": { - "Location": 0, - "Name": "", - "Format": "Text", - "Validity": "NA", - "Number": "+358444111111", - "DefaultNumber": "", - }, - "UDH": { - "Type": "ConcatenatedMessages", - "Text": b"\x05\x00\x03\x00\x02\x02", - "ID8bit": 0, - "ID16bit": -1, - "PartNumber": 2, - "AllParts": 2, - }, - "Folder": 1, - "InboxFolder": 1, - "Memory": "SM", - "Location": 2, - "Name": "", - "Number": "+358444222222", - "Text": "4567890123456789012345678901", - "Type": "Deliver", - "Coding": "Default_No_Compression", - "DateTime": datetime.datetime(2024, 3, 25, 19, 53, 56), - "SMSCDateTime": datetime.datetime(2024, 3, 25, 19, 54, 7), - "DeliveryStatus": 0, - "ReplyViaSameSMSC": 0, - "State": "UnRead", - "Class": -1, - "MessageReference": 0, - "ReplaceMessage": 0, - "RejectDuplicates": 0, - "Length": 28, - } -] diff --git a/tests/components/sms/test_gateway.py b/tests/components/sms/test_gateway.py deleted file mode 100644 index 132ba9bc1f3..00000000000 --- a/tests/components/sms/test_gateway.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Test the SMS Gateway.""" - -from unittest.mock import MagicMock - -from homeassistant.components.sms.gateway import Gateway -from homeassistant.core import HomeAssistant - -from .const import ( - NEXT_SMS_MULTIPLE_1, - NEXT_SMS_MULTIPLE_2, - NEXT_SMS_SINGLE, - SMS_STATUS_MULTIPLE, - SMS_STATUS_SINGLE, -) - - -async def test_get_and_delete_all_sms_single_message(hass: HomeAssistant) -> None: - """Test that a single message produces a list of entries containing the single message.""" - - # Mock the Gammu state_machine - state_machine = MagicMock() - state_machine.GetSMSStatus = MagicMock(return_value=SMS_STATUS_SINGLE) - state_machine.GetNextSMS = MagicMock(return_value=NEXT_SMS_SINGLE) - state_machine.DeleteSMS = MagicMock() - - response = Gateway({"Connection": None}, hass).get_and_delete_all_sms(state_machine) - - # Assert the length of the list - assert len(response) == 1 - assert len(response[0]) == 1 - - # Assert the content of the message - assert response[0][0]["Text"] == "Short message" - - -async def test_get_and_delete_all_sms_two_part_message(hass: HomeAssistant) -> None: - """Test that a two-part message produces a list of entries containing one combined message.""" - - state_machine = MagicMock() - state_machine.GetSMSStatus = MagicMock(return_value=SMS_STATUS_MULTIPLE) - state_machine.GetNextSMS = MagicMock( - side_effect=iter([NEXT_SMS_MULTIPLE_1, NEXT_SMS_MULTIPLE_2]) - ) - state_machine.DeleteSMS = MagicMock() - - response = Gateway({"Connection": None}, hass).get_and_delete_all_sms(state_machine) - - assert len(response) == 1 - assert len(response[0]) == 2 - - assert response[0][0]["Text"] == NEXT_SMS_MULTIPLE_1[0]["Text"] - assert response[0][1]["Text"] == NEXT_SMS_MULTIPLE_2[0]["Text"] diff --git a/tests/components/sms/test_init.py b/tests/components/sms/test_init.py deleted file mode 100644 index 05448ce0f57..00000000000 --- a/tests/components/sms/test_init.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Test init.""" - -from unittest.mock import Mock, patch - -from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_DEVICE -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant -from homeassistant.helpers import issue_registry as ir - -from tests.common import MockConfigEntry - - -@patch.dict( - "sys.modules", - { - "gammu": Mock(), - "gammu.asyncworker": Mock(), - }, -) -async def test_repair_issue_is_created( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, -) -> None: - """Test repair issue is created.""" - from homeassistant.components.sms import ( # noqa: PLC0415 - DEPRECATED_ISSUE_ID, - DOMAIN, - ) - - with ( - patch("homeassistant.components.sms.create_sms_gateway", autospec=True), - patch("homeassistant.components.sms.PLATFORMS", []), - ): - config_entry = MockConfigEntry( - title="test", - domain=DOMAIN, - data={ - CONF_DEVICE: "/dev/ttyUSB0", - }, - ) - - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.state is ConfigEntryState.LOADED - assert ( - HOMEASSISTANT_DOMAIN, - DEPRECATED_ISSUE_ID, - ) in issue_registry.issues - - await hass.config_entries.async_unload(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.state is ConfigEntryState.NOT_LOADED - assert ( - HOMEASSISTANT_DOMAIN, - DEPRECATED_ISSUE_ID, - ) not in issue_registry.issues