The Voip.ms SMS Integration for Home Assistant Christian Donner, April 16, 2025April 16, 2025 Introduction to Home Assistant Home Assistant is a great piece of software with many potential purposes. Out of the box, it provides on-premises monitoring of a solar system, it connects to devices, firewalls, and servers on a home network and can quickly provide detailed insights into your Internet service, WLAN, firewall, and much more. It brings various home entertainment components together in a single UI, forecasts the weather, controls your thermostats, let’s you access your security cameras’ video stream, and knows who is home and who is away. With moderate investments, you can integrate and control ZigBee and Matter IoT devices, like switches, door and window sensors, water sensors, smart locks, and motion detectors, and build very capable security monitoring systems. It can interact with more advanced devices, such as power monitoring systems, it can monitor your water and natural gas consumption in real-time, control your irrigation, pool, smart vac and other smart appliances like refrigerators, washing machines, etc. There is literally no limit to the things you can integrate. It can run a small, low-power computers like the Raspberry Pi. There are apps for Android and iPhone. The Lure of Automations Home Assistant’s true potential remains hidden until one begins to experiment with automations, though. Automations are formed by defining actions, based on trigger events and certain conditions. For example, the following automation uses these devices and entities: A HikVision dome camera installed on the front porch. The camera integration exposes the line crossing detection event and camera snapshot images. A built-in integration for daylight detection A ZigBee light switch SMS and Email services A helper event “Auto Light” that indicates that the light was turned on by the automation. My Front Door Alert Automation in Node Red It listens for a line crossing event that the camera reports. When such an event occurs, it grabs a snapshot from the camera and attaches the image to an email and MMS messages that are sent to the residents. Unfortunately, the API does not accept more than one recipient for messages, so I have to send each one individually. After sunset, it also turns on the porch light for 5 minutes and then turns it off again, but only if it was not turned on by hand. The ardent reader may notice that the diagram above is missing a check if the light is already on after motion is detected, in which case we should not turn the light off after 5 minutes. The version that is deployed at my house has this extra step. SMS As I was putting this together, I determined that SMS notifications about activities at the front door would work best for me. Push notifications through the Home Assistant Android app work well, but they require a VPN connection outside of the house. SMS allows me to respond immediately, regardless of where I am at the time, and independent of a VPN connection or the local WIFI. But SMS requires a paid service, and I did not want to pay for a Twilio subscription or one of their competitors. What I do have is phone service from Voip.ms, a Canadian provider with lots of features and very low pay-as-you-go prices for SMS messages. They do offer SOAP and REST APIs, but – I did not find an existing integration for Home Assistant. So I wrote one and named the Github project ha-voipms_sms. This integration is deployed on my server and notifies me when the line crossing event is raised. An SMS message arrives almost instantly, while an MMS with the snapshot attached takes a few seconds. Calling the SMS endpoint was relatively simple because Voip.ms’ awkward REST API uses only GET requests for that one, with all the arguments stuffed into the query string. For image attachments, GET does not work, of course, because the request URL is limited to about 2,000 characters in length and Voip.ms’ Cloudflare front will reject requests that exceed this limit before the technical limit is reached. From the Voip.ms REST API documentation: sendSMSParametersdid => [Required] DID Numbers which is sending the message (Example: 5551234567) dst => [Required] Destination Number (Example: 5551234568) message => [Required] Message to be sent (Example: 'hello John Smith' max chars: 160)OutputArray ( [status] => success [sms] => 23434 )sendMMSParametersdid => [Required] DID Numbers which is sending the message (Example: 5551234567) dst => [Required] Destination Number (Example: 5551234568) message => [Required] Message to be sent (Example: 'hello John Smith' max chars: 2048) media1 => [Optional] Url to media file (Example: 'https://voip.ms/themes/voipms/assets/img/talent.jpg?v=2' media2 => [Optional] Base 64 image encode (Example: data:image/png;base64,iVBORw0KGgoAAAANSUh...) media3 => [Optional] Empty value (Example: '' ) Requests can be made by the GET and POST methods. When sending multimedia via POST and base64, the file limit is based on the maximum allowed per message, 1.2 mb per file.When sending multimedia via GET and base64, the file limit is based on the maximum allowed by the GET request type, which supports a length of 512 characters, approximately 160kb total weight.In both GET and POST when using file URL submission, this limitation does not exist.OutputArray ( [status] => success [mms] => 23434 ) The options are – embed a link to an image (which requires that the image is uploaded and hosted somewhere else) or use a POST request and pass everything, including the base64-encoded image, as form parameters And this is where the trouble started. Getting a POST to work with a REST client was trivial. I use Talend’s Chrome extension and was able to send an image attachment within minutes (note that I redacted sensitive data in the screen shot below). Python’s aiohttp library and the trouble with multipart/form-data So I was optimistic that adding a send_mms method to my integration (that already sent SMS messages at this point) would be trivial. But no. I then spent the next 2 days trying to write a Python method that does the following: curl -i -X POST \ -H "Content-Type:multipart/form-data" \ -F "api_password=[voip.ms API password]" \ -F "api_username=[voip,ms user account]" \ -F "did=[sender DID]" \ -F "dst=[recipient number]" \ -F "media1=data:image/jpeg;base64,/9j/ [truncated for brevity]" \ -F "message=Hello MMS!" \ -F "method=sendMMS" \ 'https://voip.ms/api/v1/rest.php' My initial instinct was to instruct ChatGPT with a prompt to write me a Python method that produces the above request, using aiohttp. Home Assistant balks if an integration does blocking HTTP calls, so the requests library cannot be used here. I learned this earlier with the send_sms endpoint. It did not work. What made debugging difficult is the fact that aiohttp has no hook that allows us to inspect the request before it goes out on the wire. I kept getting a 200 response with {"status":"missing_method"}, which suggests that the parameters were not properly passed or serialized. and with my app running on a RPi in a rack in my basement, my appetite for doing packet inspection on the device was limited. ChatGPT did not come up with a working solution. I realized at some point that aiohttp.FormData() might be the wrong way to go about this and I should instead use MultipartWriter(). ChatGPT thought that this was a good idea, but the error remained. However, I had noticed before that Voip.ms redirects calls to www.voip.ms to voip.ms and therefore I should have caught much sooner that an www had snuck into my endpoint URL. It was this redirect that caused the error. The request body is lost when a redirect happens and the server did not see my input parameters, but there was no indication of a redirect in the log output, even with the log level set to DEBUG. At that point I had switched back to using FormData() for my request. I got XML or HTML responses now, but it still did not work. With MultipartWriter() I eventually got the MMS message to be sent. Here is the final working Python code: media_data = await get_base64_data(image_path) form_data = { 'api_username': str(user), 'api_password': str(password), 'did': str(sender_did), 'dst': str(recipient), 'message': str(message), 'method': str('sendMMS'), 'media1': str(media_data) } async with aiohttp.ClientSession() as session: with aiohttp.MultipartWriter("form-data") as mp: for key, value in form_data.items(): part = mp.append(value) part.set_content_disposition('form-data', name=key) async with session.post(REST_ENDPOINT, data=mp) as response: response_text = await response.text() if response.status == 200: _LOGGER.info("voipms_sms: MMS sent successfully: %s", response_text) else: _LOGGER.error("voipms_sms: Failed to send MMS. Status: %s, Response: %s", response.status, response_text) The final code places all parameters into a dictionary, including the base64-encoded image with the proper header, then iterates over the dict, appends each parameter as a Multipart value, and then sets the content disposition to form-data. This method reads the image file and prepares the encoded string: async def get_base64_data(image_path): def encode(): mime_type, _ = mimetypes.guess_type(image_path) if not mime_type: mime_type = 'application/octet-stream' with open(image_path, 'rb') as f: encoded = base64.b64encode(f.read()).decode() return f"data:{mime_type};base64,{encoded}" return await asyncio.to_thread(encode) Now, it works as designed, and an MMS message arrives within seconds when I get a package delivery: Related Posts:Enphase Envoy Local AccessOpenVPNThe Great Cat Litter Poop OffA box of SUTABComputer Build 2025 Free Software Home Assistant Open Source Weekend Warrior