mirror of https://github.com/home-assistant/core
212 lines
7.6 KiB
Python
212 lines
7.6 KiB
Python
"""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
|