Jag köpte nyligen en Wi-Fi uppkopplad fjärrströmbrytare från Cleverio, som säljs av Kjell & Co, mer specifikt den här modellen.

Följer man de officiella instruktionerna får man tips om en app att använda för att styra och parkoppla strömbrytaren.

Efter lite nyfikenhet förstod jag att produkterna från Cleverio i själva verket är omprofilerade Tuya-enheter, vilket signifikant ökar möjligheterna för att hitta andra som undersökt produkterna närmare.

Jag har två mål:

  1. Kunna styra strömbrytaren utan appen, genom något form av API (gärna webbaserat och REST)
  2. Blockera strömbrytaren från att ansluta till Internet, men fortfarande fungera på det lokala nätverket.

(1) är framförallt för att jag vill kunna göra vad jag vill rent hemautomatiseringsmässigt, och (2) är framförallt av säkerhetsskäl då internetuppkopplade IoT-enheter sällan har särskilt bra säkerhet.

Existerande projekt

Det finns en hel del intressanta tidigare projekt inom området, dessa är de jag kikat på och fått inspiration från.

  • TinyTuya: Ett Python-bibliotek för att kommunicera med Tuya-baserade enheter.
  • rust-tuyapi: Ett Rust-bibliotek för att kommunicera med Tuya-baserade enheter.
  • rust-async-tuyapi: Ett Rust-bibliotek (async) för att kommunicera med Tuya-baserade enheter, baserat på rust-tuyapi ovan.

Hitta strömbrytarens ID och nyckel

Första steget i att ta kontroll över sin strömbrytare är att hitta brytarens ID och hemliga nyckel. Dessa behövs för att kunna kommunicera med brytaren direkt på samma LAN.

Det finns flera sätt att hitta ID och nyckel som behövs. Du behöver bara använda ett sätt.

Hur jag gjorde

Eftersom jag redan hade en gammal oanvänd rootad Androidtelefon hemma, inspirerades jag av denna guiden, och gjorde såhär:

  1. Installerade en gammal version av appen, nämligen 3.6.1. Nej, det fungerar inte med nyaste versionen. Jag använde adb install xxxxxx.apk.
  2. Loggade in med mitt konto
  3. Parkopplade min strömbrytare
  4. Hämtade hem filen /data/data/com.tuya.smartlife/shared_prefs/preference_global_keyeuXXXXXXXX.xml till min dator och namngav den codes.xml. (Jag använde adb shell och cat för att få filens innehåll)
  5. Körde skriptet transform.py från guiden ovan.
  6. id och key är de delar du behöver för att kunna styra strömbrytarna själv.

Andra guider

  1. Följ instruktionerna från TinyTuya. Kortfattat: registrera dig som Tuya-utvecklare hämta hem koderna från deras utvecklarportal.
  2. Följ instruktionerna från tuya_code_extract. Kortfattat: installera Smart life-appen i en Android-emulator, logga in med ditt konto, och hämta nycklarna.

REST API

Eftersom jag ville kunna styra strömbrytaren flexibelt byggde jag ett litet REST API, skrivet i Rust med axum, kring rust-async-tuyapi.

API:t stödjer flera strömbrytare (numreras från 0 och uppåt), och startas med hjälp av ID, nyckel, och IP-adress för brytaren.

Källkoden till projektet finns här: https://github.com/zozs/tuya-web.

Kortfattat så exponerar den dessa endpoints:

  • POST /outlet/:outlet_id: Växlar uttaget mellan av och på
  • PUT /outlet/:outlet_id/true: Slår på strömmen
  • PUT /outlet/:outlet_id/false: Slår av strömmen
  • GET /outlet/:outlet_id: Returnerar nuvarande tillstånd på uttaget

Detta gör att jag dels kan koppla POST-rutten till en vanlig knapp, i mitt fall en Flic-knapp. Samtidigt kan jag ha cronjobs som slår på och av ett uttag vid vissa tidpunkter. I mitt fall styr jag en växtlampa med följande Kubernetes CronJob.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: tuya-growing-light-on
spec:
  schedule: "0 9 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: curl
            image: quay.io/curl/curl
            command: ["curl", "-v", "-X", "PUT", "http://tuya-web.default.svc.cluster.local/outlet/0/true"]
          restartPolicy: OnFailure
---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: tuya-growing-light-off
spec:
  schedule: "0 17 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: curl
            image: quay.io/curl/curl
            command: ["curl", "-v", "-X", "PUT", "http://tuya-web.default.svc.cluster.local/outlet/0/false"]
          restartPolicy: OnFailure

Undersök och blockera internettillgång

När allt är klart blockerade jag internettillgången för strömbrytarens IP-adress i min router. I mitt fall med en brandväggsregel för PF.

#/etc/pf.conf

# Block smart plug from accessing internet
block in quick on guestnet from { 10.0.5.50 } to !(guestnet:network)

Samtidigt passade jag på att logga de DNS förfrågningar som gjordes i unbound.

#/etc/unbound.conf

log-queries: yes
verbosity: 2

Loggarna kommer nedanför, men sammanfattningsvis:

  • Det skickas en massa broadcasts på port 6667/UDP. Det verkar vara kopplat till auto-discovery för att appen ska kunna hitta strömbrytaren på samma Wi-Fi. Verkar inte göra något att dessa blockeras.
  • Den gör en ny DHCP förfrågan ungefär var femte minut. Jag misstänker att det är för att den inte kan ansluta till molntjänsten? Oavsett vilket verkar det fungera ändå.
  • Den gör DNS förfrågningar till m2.tuyaeu.com, och sen försöker den ansluta dit. Gissningsvis det som är den faktiska molntjänsten för att styra den med appen.

Loggar

Mar 28 10:36:51 zone dhcpd[24647]: DHCPOFFER on 10.0.5.50 to 1c:90:ff:0a:72:88 via vlan2
Mar 28 10:36:51 zone dhcpd[24647]: DHCPREQUEST for 10.0.5.50 from 1c:90:ff:0a:72:88 via vlan2
Mar 28 10:36:51 zone dhcpd[24647]: DHCPACK on 10.0.5.50 to 1c:90:ff:0a:72:88 via vlan2
Mar 28 10:37:03 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:37:08 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:37:13 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:37:18 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:37:23 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:37:28 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:37:33 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:37:38 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:37:43 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:37:44 zone unbound: [85358:0] info: 10.0.5.50 m2.tuyaeu.com. A IN
Mar 28 10:37:44 rule 0/(match) block in on vlan2: 10.0.5.50.40188 > 35.156.42.116.8886: S 357465146:357465146(0) win 4380 <mss 1460>
Mar 28 10:37:46 rule 0/(match) block in on vlan2: 10.0.5.50.40188 > 35.156.42.116.8886: S 357465146:357465146(0) win 4380 <mss 1460>
Mar 28 10:37:48 rule 0/(match) block in on vlan2: 10.0.5.50.40188 > 35.156.42.116.8886: S 357465146:357465146(0) win 4380 <mss 1460>
Mar 28 10:37:48 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:37:50 rule 0/(match) block in on vlan2: 10.0.5.50.40188 > 35.156.42.116.8886: S 357465146:357465146(0) win 4380 <mss 1460>
Mar 28 10:37:52 rule 0/(match) block in on vlan2: 10.0.5.50.40188 > 35.156.42.116.8886: S 357465146:357465146(0) win 4380 <mss 1460>
Mar 28 10:37:53 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:37:54 rule 0/(match) block in on vlan2: 10.0.5.50.40188 > 35.156.42.116.8886: S 357465146:357465146(0) win 4380 <mss 1460>
Mar 28 10:37:56 rule 0/(match) block in on vlan2: 10.0.5.50.40188 > 35.156.42.116.8886: S 357465146:357465146(0) win 4380 <mss 1460>
Mar 28 10:37:58 rule 0/(match) block in on vlan2: 10.0.5.50.40188 > 35.156.42.116.8886: S 357465146:357465146(0) win 4380 <mss 1460>
Mar 28 10:37:58 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:38:00 rule 0/(match) block in on vlan2: 10.0.5.50.40188 > 35.156.42.116.8886: S 357465146:357465146(0) win 4380 <mss 1460>
Mar 28 10:38:03 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172