Control Yokis roller shutters in Home Assistant via the YnO cloud (no local Hub token)
Every Yokis integration I could find (the Homebridge plugin, HACS projects) talks to the Hub’s local API, which means extracting an HTTP Basic token by sniffing the YnO app’s traffic. On some setups that local token is not your YnO credentials and can’t be obtained easily.
This method uses the cloud API at yokiscloud.fr with your plain YnO login/password. It exposes each shutter as a cover entity (0‑100 % position), so you get dashboard control, automations, and — through the HomeKit bridge — the Apple Home app and Siri.
▎
Warnings: the cloud API is undocumented (/api/1_28/) and may change without notice. Your YnO credentials are stored in clear text under /config. Community solution, not affiliated with Yokis, use at your own risk.
What was reverse-engineered
- Auth: POST https://www.yokiscloud.fr/api/1_28/individuals/login with {“login”,“password”} → returns id, token (send it in a token: header, not Authorization), and boxes.boxId. The token expires (must be refreshed).
- Modules: GET …/{userId}/box/{boxId} → field function: 6 = roller shutter, 4 = relay/toggle, 7 = hub.
- State: GET …/{userId}/box/{boxId}/modulestable → data.table, field var = position 0‑100 (0 = closed, 100 = open, no inversion), alive (0 = unreachable → var is stale).
- Command: POST …/{userId}/box/{boxId}/commands with {“cmd”:“command.xml?action=order&id=&order=”}
- order=up (open), order=down (close), order=varX&ext1=<0..100> (exact position).
- The cloud does not validate the command (returns 200 to anything) and relays it to the Hub.
Step 1 — the script
Put this file at /config/yokis/yokis.py (standard library only, no dependencies):
#!/usr/bin/env python3
“”“Yokis Cloud ↔ Home Assistant bridge. Commands: get/set/open/close/list.
Credentials in a 2-line file (login then password):
YOKIS_CREDS env, else /yokis_creds, else ~/.yokis_creds.
The token is cached and refreshed automatically.”“”
import json, os, sys, time, urllib.request, urllib.error
BASE = “https://www.yokiscloud.fr/api/1_28/individuals”
def _creds_path():
env = os.environ.get(“YOKIS_CREDS”)
if env: return env
here = os.path.join(os.path.dirname(os.path.abspath(file)), “yokis_creds”)
return here if os.path.exists(here) else os.path.expanduser(“~/.yokis_creds”)
CREDS = _creds_path()
CACHE = os.path.join(os.path.dirname(CREDS) or “.”, “yokis_token.cache”)
def _req(url, method=“GET”, payload=None, token=None):
data = json.dumps(payload).encode() if payload is not None else None
r = urllib.request.Request(url, data=data, method=method)
r.add_header(“Content-Type”, “application/json”); r.add_header(“Accept”, “application/json”)
if token: r.add_header(“token”, token)
try:
with urllib.request.urlopen(r, timeout=30) as resp:
return resp.status, resp.read().decode()
except urllib.error.HTTPError as e:
return e.code, e.read().decode()
except Exception as e:
return None, repr(e)
def _login():
lines = open(CREDS).read().split(“\n”)
login, password = lines[0].strip(), lines[1] if len(lines) > 1 else “”
st, body = _req(f"{BASE}/login", “POST”, {“login”: login, “password”: password})
if st != 200: raise SystemExit((2, f"login failed HTTP {st}"))
b = json.loads(body)
sess = {“uid”: b[“id”], “token”: b[“token”], “box”: b[“boxes”][0][“boxId”], “ts”: time.time()}
try:
old = os.umask(0o077)
with open(CACHE, “w”) as f: json.dump(sess, f)
os.umask(old)
except Exception: pass
return sess
def _session():
try:
sess = json.load(open(CACHE))
if all(k in sess for k in (“uid”, “token”, “box”)): return sess
except Exception: pass
return _login()
def _table(sess):
st, body = _req(f"{BASE}/{sess[‘uid’]}/box/{sess[‘box’]}/modulestable", token=sess[“token”])
if st != 200: return None
try: return json.loads(body)[“data”][“table”]
except Exception: return None
def _table_reauth():
sess = _session(); t = _table(sess)
if t is None: sess = _login(); t = _table(sess)
return sess, t
def _order(uid, order, ext1=None):
sess = _session()
cmd = f"command.xml?action=order&id={uid}&order={order}"
if ext1 is not None: cmd += f"&ext1={ext1}"
url = f"{BASE}/{sess[‘uid’]}/box/{sess[‘box’]}/commands"
st, _ = _req(url, “POST”, {“cmd”: cmd}, sess[“token”])
if st != 200: # token likely expired: refresh and retry once
sess = _login()
url = f"{BASE}/{sess[‘uid’]}/box/{sess[‘box’]}/commands"
st, _ = _req(url, “POST”, {“cmd”: cmd}, sess[“token”])
if st != 200: raise SystemExit((2, f"command refused HTTP {st}"))
def main(argv):
if len(argv) < 2: raise SystemExit((1, doc))
action = argv[1]
if action == “get”:
_, table = _table_reauth()
if table is None: raise SystemExit((2, “modulestable unavailable”))
for e in table:
if e.get(“uid”) == argv[2]: print(int(e.get(“var”, 0))); return
raise SystemExit((2, f"uid {argv[2]} not found"))
if action == “set”:
_order(argv[2], “varX”, max(0, min(100, int(argv[3])))); print(argv[3]); return
if action == “open”:
_order(argv[2], “up”); print(100); return
if action == “close”:
_order(argv[2], “down”); print(0); return
if action == “list”:
sess = _session()
st, body = _req(f"{BASE}/{sess[‘uid’]}/box/{sess[‘box’]}“, token=sess[“token”])
if st != 200:
sess = _login()
st, body = _req(f”{BASE}/{sess[‘uid’]}/box/{sess[‘box’]}“, token=sess[“token”])
data = json.loads(body); mods = data.get(“modules”) if isinstance(data, dict) else data
for m in mods: print(f”{m.get(‘uid’)}\t{m.get(‘function’)}\t{m.get(‘name’)}“)
return
raise SystemExit((1, f"unknown action: {action}”))
if name == “main”:
try: main(sys.argv)
except SystemExit as e:
if isinstance(e.code, tuple): print(e.code[1], file=sys.stderr); sys.exit(e.code[0])
raise
Step 2 — credentials + discover your modules
Create /config/yokis/yokis_creds (two lines):
[email protected]
your_YnO_password
Then list your modules (inside the HA container) — note the uids whose function is 6 (your shutters):
python3 /config/yokis/yokis.py list
uid function name
A1B2C3D4 6 Living room shutter
Step 3 — configuration.yaml
The sensor gives the real position but is only polled every 30 s, so on its own the HomeKit slider “snaps back”. We store the position in an input_number updated immediately on command, re-synced by the automation in step 4. Replace A1B2C3D4 with your uid, repeat per shutter:
shell_command:
yokis_open: “python3 /config/yokis/yokis.py open {{ uid }}”
yokis_close: “python3 /config/yokis/yokis.py close {{ uid }}”
yokis_set: “python3 /config/yokis/yokis.py set {{ uid }} {{ position }}”
command_line:
- sensor:
name: shutter_living_pos
command: “python3 /config/yokis/yokis.py get A1B2C3D4”
unit_of_measurement: “%”
scan_interval: 30
value_template: “{{ value | int(0) }}”
input_number:
yokis_shutter_living:
name: “Living room shutter position”
min: 0
max: 100
step: 1
mode: box
template:
- cover:
- name: “Living room shutter”
unique_id: yokis_A1B2C3D4
device_class: shutter
position: “{{ states(‘input_number.yokis_shutter_living’) | int(0) }}”
open_cover:- action: input_number.set_value
target: { entity_id: input_number.yokis_shutter_living }
data: { value: 100 } - action: shell_command.yokis_open
data: { uid: A1B2C3D4 }
close_cover: - action: input_number.set_value
target: { entity_id: input_number.yokis_shutter_living }
data: { value: 0 } - action: shell_command.yokis_close
data: { uid: A1B2C3D4 }
set_cover_position: - action: input_number.set_value
target: { entity_id: input_number.yokis_shutter_living }
data: { value: “{{ position }}” } - action: shell_command.yokis_set
data:
uid: A1B2C3D4
position: “{{ position }}”
- action: input_number.set_value
- name: “Living room shutter”
▎ Naming convention: the sensor pos, the input_number yokis and the cover must line up (here slug = shutter_living). The automation below relies on it.
Step 4 — automations.yaml (re-sync real position)
- id: yokis_sync_positions
alias: Yokis - sync shutter positions
mode: queued
max: 20
trigger: condition:- condition: template
value_template: “{{ trigger.to_state.state not in [‘unknown’,‘unavailable’,‘’] }}”
action: - action: input_number.set_value
target:
entity_id: “{{ trigger.entity_id | replace(‘sensor.’,‘input_number.yokis_’) | replace(‘_pos’,‘’) }}”
data:
value: “{{ trigger.to_state.state | int(0) }}”
- condition: template
Step 5 — HomeKit / Siri (optional)
homekit:
- name: Yokis Shutters
filter:
include_entities:
- cover.shutter_living
Restart HA, then pair the bridge from Settings → Devices & Services → HomeKit Bridge (QR code). The shutters answer Siri (“set the living room shutter to 50 %”).
Known limitations
- Undocumented cloud API → may break on any Yokis update.
- Requires internet (no control if the cloud is down).
- A battery / unreachable module (alive=0) returns a stale position.
- Polling: one cloud call per shutter every 30 s.
- Only shutters (function=6) handled here; relays (function=4) would use order=on/off.
Credits
Reverse-engineering of the cloud path (token: header, order=varX&ext1, var = position). Builds on prior community work on the Yokis Hub’s local API. Not affiliated with Yokis.