169 lines
6.0 KiB
Python
169 lines
6.0 KiB
Python
"""The sms gateway to interact with a GSM modem."""
|
|
import logging
|
|
|
|
import gammu # pylint: disable=import-error
|
|
from gammu.asyncworker import GammuAsyncWorker # pylint: disable=import-error
|
|
|
|
from homeassistant.core import callback
|
|
|
|
from .const import DOMAIN
|
|
|
|
_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
|
|
self._hass = hass
|
|
|
|
async def init_async(self):
|
|
"""Initialize the sms gateway asynchronously."""
|
|
try:
|
|
await self._worker.set_incoming_sms_async()
|
|
except gammu.ERR_NOTSUPPORTED:
|
|
_LOGGER.warning("Your phone does not support incoming SMS notifications!")
|
|
else:
|
|
await self._worker.set_incoming_callback_async(self.sms_callback)
|
|
|
|
def sms_callback(self, state_machine, callback_type, callback_data):
|
|
"""Receive notification about incoming event.
|
|
|
|
@param state_machine: state machine which invoked action
|
|
@type state_machine: gammu.StateMachine
|
|
@param callback_type: type of action, one of Call, SMS, CB, USSD
|
|
@type callback_type: string
|
|
@param data: event data
|
|
@type data: hash
|
|
"""
|
|
_LOGGER.debug(
|
|
"Received incoming event type:%s,data:%s", callback_type, callback_data
|
|
)
|
|
entries = self.get_and_delete_all_sms(state_machine)
|
|
_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)
|
|
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 = 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)
|
|
|
|
# pylint: disable=no-self-use
|
|
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
|
|
all_parts_arrived = False
|
|
_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"]
|
|
is_single_part = all_parts == 0
|
|
is_multi_part = 0 <= all_parts < start_remaining
|
|
_LOGGER.debug("All parts:%i", all_parts)
|
|
_LOGGER.debug("Part Number:%i", part_number)
|
|
_LOGGER.debug("Remaining:%i", remaining)
|
|
all_parts_arrived = is_multi_part or is_single_part
|
|
_LOGGER.debug("Start all_parts_arrived:%s", all_parts_arrived)
|
|
start = False
|
|
else:
|
|
entry = state_machine.GetNextSMS(
|
|
Folder=0, Location=entry[0]["Location"]
|
|
)
|
|
|
|
if all_parts_arrived or force:
|
|
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")
|
|
else:
|
|
_LOGGER.debug("Not all parts have arrived")
|
|
break
|
|
|
|
except gammu.ERR_EMPTY:
|
|
# error is raised if memory is empty (this induces wrong reported
|
|
# memory status)
|
|
_LOGGER.info("Failed to read messages!")
|
|
|
|
# Link all SMS when there are concatenated messages
|
|
entries = gammu.LinkSMS(entries)
|
|
|
|
return 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 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)
|
|
await gateway.init_async()
|
|
return gateway
|
|
except gammu.GSMError as exc:
|
|
_LOGGER.error("Failed to initialize, error %s", exc)
|
|
return None
|