Smanos w100 alarm panel

I’ve been using a closed source alarm panel from a chinese oem called smanos.
Looks like it is just a rebranded chuango for the western markets.
The panel is w100 and provides no api.
Came upon a security cve about this panel that it makes it vulnerable from an attacker on the same network.
I managed making use of that cve to get the payload for arming,disarming and setting to home mode l.
These are enough data to automate the panel with a bash script using netcat.
Its a 2018 panel so if anyone left over with it this might be helpfull.

You’ll need your device id.
Save CVE-2019-13361/poc.py at 75712ea4d6308d2c2d5bc3693b27170da6869cc9 · lodi-g/CVE-2019-13361 · GitHub as a .py file

usage: a [-h] [-p PORT]
ip wifi_ssid wifi_password

e.g. script.py 192.168.1.1 mywifi 123456

It will return:

set_wifi: receiving:    b'CGWPSC030000deviceid**\r'
set_wifi: device_id:    b'xxxxxx**'
disarm: sending:        b'CGWPCS53xxxxxxxxxx**2'
disarm: receiving:      b'CGWPSC53xxxxxxxxxxxxx**1001\r'

Your payload is what the device returned as "disarm: sending: "

The number after ** is the Mode: 0/1/2 ; Disarm/Arm/home

Knowing the payload you can use it to any script to change the panels mode

I created 3 scripts under config/shell_scripts/

w100_arm.py
w100_disarm.py
w100_ome.py

And made them executable chmod +x

They are identical only the last number of the payload changes.

#!/bin/bash                                     
ip="192.168.1.1"
ssid="mywifi"
pswd="123456"
port=60003
payload="CGWPCS53xxxxxxxxxxxxxxc**0"

# Function to send payload and exit after 10 second
send_payload() {
    { echo -n "$payload" | nc -w 10 "$ip" "$port"; } > /dev/null 2>&1 &
    sleep 1
    kill %1
}

# Main
send_payload

And added to the configuration.yaml

shell_command:
  w100_home: /config/shell_scripts/w100_home.sh
  w100_disarm: /config/shell_scripts/w100_disarm.sh
  w100_arm: /config/shell_scripts/w100_arm.sh

Restart home assistant

You can use the scripts as actions in automation.

My example with voice assistant

alias: W100 arm
description: ""
trigger:
  - platform: conversation
    command:
      - alarm on
      - Turn on alarm
      - Turn-on alarm
      - Turnon alarm
      - Turn on the alarm
condition: []
action:
  - service: shell_command.w100_arm
    data: {}
mode: single

**afterthoughts:

  1. I’m totally new with home assistant and not an advanced user at all. Scripts and integration my not be polished .
  2. Thanks to @tetele and @Tinkerer on the discord server that helped me out

#todo:
Decompiled the android app.
Uploaded the relative java class containg the payload info W100 - Pastebin.com
It may contain addidtional info for further use cases.

2 Likes

Hi @ippocratis ! Thank you for this - this has been tremendously helpful.

Not sure if you’re still using your W100 panel, but I’ve also been working on reverse-engineering it especially as the notifications server has gone offline in the past weeks.

If you’d like to connect, let me know. Happy to share where I’m at.

Hey @pjft if you have any insights regarding reverse engineering this panel I’d love to hear it

Looks like the panel push plain tcp messages on 60003

Created a python listener

#!/usr/bin/env python3
import socket

def listen_notifications(s):
    while True:
        try:
            data = s.recv(1024)
            if not data:
                break
            print(f"Notification received: {data}")
        except Exception as e:
            print("Error receiving:", e)
            break

def main():
    ip = "192.168.1.100"   # replace with your panel IP
    port = 60003           # default port

    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect((ip, port))
        print("Connected, waiting for notifications...")
        listen_notifications(s)

if __name__ == '__main__':
    main()

and triggered a magnetic contact the script printed

 Notification received: b'CGWPSC01010112345A6**

020202202508231910510\r'

So I have

  • CGWPSC01 → Protocol header (type of message, probably “event/notification”).

  • 0101 → Could be a zone number / device number.

  • 12345A6 → Very likely my device ID (matches what I extracted in my Wi-Fi setup step).

  • ** → Mode / event separator (like in disarm/arm payloads).

  • 020202 → This is almost certainly the event code (magnetic contact open/triggered).

  • 20250823191051 → That looks like a timestamp:

    • 2025 → year
    • 08 → month
    • 23 → day
    • 19:10:51 → time (exactly when you triggered it).
  • 0\r → End of packet marker.

From here I tried simple ntfy notifications extending the tcp listener script with ntfy and human readable text making sure the ntfy topic mach in the script and in the iOS android app

#!/usr/bin/env python3
import socket
import requests

def send_ntfy(message, topic="myalarm"):
    url = f"https://ntfy.sh/{topic}"
    try:
        r = requests.post(url, data=message.encode("utf-8"))
        print(f"Sent notification: {message} (status {r.status_code})")
    except Exception as e:
        print("Error sending ntfy notification:", e)

def parse_notification(data):
    msg = data.decode(errors="ignore").strip()
    if msg.startswith("CGWPSC01"):
        device_id = msg[12:20]
        event_code = msg[22:28]   # e.g. 020202
        timestamp = msg[28:42]    # YYYYMMDDHHMMSS

        events = {
            "020202": "Magnetic contact triggered",
            "010101": "Panel armed",
            "000000": "Panel disarmed",
            # Add more as you learn the codes
        }

        return f"{events.get(event_code, f'Unknown event ({event_code})')} at {timestamp}", msg
    return "Unparsed event", msg

def listen_notifications(s):
    while True:
        try:
            data = s.recv(1024)
            if not data:
                break
            parsed, raw = parse_notification(data)
            print(f"Notification received: {raw}")
            print(f"Parsed: {parsed}")
            send_ntfy(parsed)
        except Exception as e:
            print("Error receiving:", e)
            break

def main():
    ip = "192.168.1.100"   # <-- replace with your panel IP
    port = 60003           # <-- default port

    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect((ip, port))
        print("Connected, waiting for notifications...")
        listen_notifications(s)

if __name__ == '__main__':
    main()

Triggered the same magnetic contact and boom I had a notification on my phone

From here I think adding home assistant buttons and notifications will be easy . I just have to find all my sensors device zone numbers wvwnt codes etc .

I just have to trigger each on and read the print out .

I’ll come up later updating this guide when I got some time and all is ready

@pjft I’ve put it all together in my blog since I can’t edit my initial message here

Have a look

Again if you have any more ideas or you have come to any additional findings please post them here

Hi man, thank you very much for this explanation, i found a way to get alarm status from Home Assistant, you can check it:

  w100_status: >
    python3 -c "import socket; s = socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.settimeout(5); s.connect(('192.168.5.160', 60003)); s.sendall(b'CGWPCS540000<DEVICE_ID>27'); s.close()"

This will answer you:

"CGWPSC540000<DEVICE_ID>27010000001" = "disarmed"
"CGWPSC540000<DEVICE_ID>27210000001" = "home_armed"
"CGWPSC540000<DEVICE_ID>27110000001" = "armed"

And i also create a input select

input_select:
  smanos_mode:
    name: Estado Real Alarma
    options:
      - disarmed
      - armed
      - home_armed

So then i created a card with status:

type: horizontal-stack
cards:
  - type: custom:button-card
    entity: input_select.smanos_mode
    name: Conectar
    icon: mdi:shield-lock
    tap_action:
      action: call-service
      service: script.alarm_arm
    state:
      - value: armed
        styles:
          card:
            - background-color: "#27ae60"
            - color: white
          icon:
            - color: white
    styles:
      card:
        - background-color: var(--ha-card-background, var(--card-background-color, "#34495e"))
        - color: var(--primary-text-color)
        - border-radius: 12px
        - border: 1px solid var(--divider-color, rgba(0,0,0,0.1))
        - box-shadow: var(--ha-card-box-shadow, 0px 2px 4px rgba(0,0,0,0.1))
        - transition: all 0.3s ease
      icon:
        - color: var(--primary-text-color)
        - width: 40px
      grid:
        - grid-template-areas: "\"i\" \"n\""
        - grid-template-rows: 1fr min-content
      name:
        - font-size: 14px
        - font-weight: bold
        - padding-bottom: 5px
  - type: custom:button-card
    entity: input_select.smanos_mode
    name: Desconectar
    icon: mdi:shield-off
    tap_action:
      action: call-service
      service: script.alarm_disarm
    state:
      - value: disarmed
        styles:
          card:
            - background-color: "#c54133"
            - color: white
          icon:
            - color: white
    styles:
      card:
        - background-color: var(--ha-card-background, var(--card-background-color, "#34495e"))
        - color: var(--primary-text-color)
        - border-radius: 12px
        - border: 1px solid var(--divider-color, rgba(0,0,0,0.1))
        - box-shadow: var(--ha-card-box-shadow, 0px 2px 4px rgba(0,0,0,0.1))
        - transition: all 0.3s ease
      icon:
        - color: var(--primary-text-color)
        - width: 40px
      grid:
        - grid-template-areas: "\"i\" \"n\""
        - grid-template-rows: 1fr min-content
      name:
        - font-size: 14px
        - font-weight: bold
        - padding-bottom: 5px
  - type: custom:button-card
    entity: input_select.smanos_mode
    name: Baixo
    icon: mdi:car-side
    tap_action:
      action: call-service
      service: script.alarm_home
    state:
      - value: home_armed
        styles:
          card:
            - background-color: "#0c93bf"
            - color: white
          icon:
            - color: white
    styles:
      card:
        - background-color: var(--ha-card-background, var(--card-background-color, "#34495e"))
        - color: var(--primary-text-color)
        - border-radius: 12px
        - border: 1px solid var(--divider-color, rgba(0,0,0,0.1))
        - box-shadow: var(--ha-card-box-shadow, 0px 2px 4px rgba(0,0,0,0.1))
        - transition: all 0.3s ease
      icon:
        - color: var(--primary-text-color)
        - width: 40px
      grid:
        - grid-template-areas: "\"i\" \"n\""
        - grid-template-rows: 1fr min-content
      name:
        - font-size: 14px
        - font-weight: bold
        - padding-bottom: 5px
title: Alarma

and a button-card that reloads every time that app or browser loads

type: custom:button-card
icon: mdi:refresh
aspect_ratio: 20/2
styles:
  card:
    - padding: 1%
custom_fields:
  ejecutor: |
    [[[
      const ahora = Date.now();
      if (!window.lastRun || (ahora - window.lastRun) > 10000) {
        window.lastRun = ahora;
        hass.callService('script', 'alarm_status');
      }
    ]]]
tap_action:
  action: call-service
  service: script.alarm_status

I also edit the python to use appDaemon and to avoid requests libraries, and add the status changed like this:

                        if "CGWPSC530000<DEVICE_ID>**1001" in msg:
                            self.log("Confirmación de comando recibida. Ejecutando shell_command w100_status...")
                            self.call_service("script/alarm_status")

                        if "CGWPSC540000<DEVICE_ID>2701" in msg:
                            self.set_state("input_select.smanos_mode", state="disarmed")
                            self.log("Estado detectado: Desarmado")
                        if "CGWPSC540000<DEVICE_ID>2721" in msg:
                            self.set_state("input_select.smanos_mode", state="home_armed")
                            self.log("Estado detectado: Home Armed")
                        if "CGWPSC540000<DEVICE_ID>2711" in msg:
                            self.set_state("input_select.smanos_mode", state="armed")
                            self.log("Estado detectado: Armed")

Here is what is look now: