Files
hotspring/custom_components/hotspring/constants.py
T
Hank MuellerandClaude Opus 5 cde2bbc772 Add lights/jets/salt control, app-traffic mimicry, and optional local read path
- New entities: number (salt output level 0-10), switch (lights, jets); the
  read-only salt sensor is replaced by the settable number.
- Look identical to the official app on the wire: send the app's Dalvik UA on
  REST, okhttp/4.12.0 on the mTLS creds fetch, and "mqtt-<uuid>" client ids.
- Consolidate every constant value into constants.py (well commented); protocol.py
  keeps only the command builders; const.py removed.
- Optional local read path: a `local_status_url` config option polls the dongle's
  local /status endpoint for telemetry instead of the cloud MQTT subscription
  (control stays cloud MQTT). State getters handle both schemas.

No private/internal values in the repo (the spa IP is runtime config only).

Fixes bug-41mqxddz7zeh

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F6B8b3iYNv6QUftK2FDfYb
2026-09-01 15:12:24 +00:00

96 lines
4.1 KiB
Python

"""All constant values for the Hot Spring integration, in one place.
Nothing here is a secret. Account credentials come from the config entry and the
vendor mutual-TLS client certificate is loaded from host files (see the README).
"""
from __future__ import annotations
# ============================================================================
# Client-mimicry identifiers
# ----------------------------------------------------------------------------
# We make every request to Watkins' cloud look exactly like the official
# Android app, so our traffic is indistinguishable from it and does not get
# flagged. Values below were taken from the decompiled app (org.watkins.hotspring).
# ============================================================================
# The app's REST calls use java.net HttpURLConnection and set NO explicit
# User-Agent, so they carry Android's default Dalvik UA. We send a realistic
# modern-Android Dalvik UA in that same format on the REST endpoints.
DALVIK_UA = "Dalvik/2.1.0 (Linux; U; Android 14; Pixel 7 Build/AP2A.240905.003)"
# The app's mutual-TLS credential fetch uses OkHttp, whose default UA is
# "okhttp/<version>". The app bundles OkHttp 4.12.0 (from okhttp3 Util.java).
OKHTTP_UA = "okhttp/4.12.0"
# The app builds its MQTT client id as "mqtt-" + a random UUID
# (MQTTMTLSConnectionKt.generateClientId). We match that format.
MQTT_CLIENT_ID_PREFIX = "mqtt-"
# The app sends "application/json" for both Content-Type and (lowercase) accept.
CONTENT_TYPE_JSON = "application/json"
# ============================================================================
# Vendor cloud endpoints (reverse-engineered from the app)
# ============================================================================
# REST API — Django SimpleJWT.
API_BASE = "https://iotsupportportalapi.watkinsmfg.com"
LOGIN_PATH = "/api/v1/login/"
REFRESH_PATH = "/api/v1/refresh/"
SPA_DETAILS_PATH = "/user_spa_details/"
# Mutual-TLS endpoint that returns the (rotating) HiveMQ broker credentials.
# Its server cert is signed by the firmware CA bundled in the app, so it is
# verified against the CA PEM on the host rather than the public trust store.
MQTT_CREDS_URL = "https://iotfirmwareota.watkinsmfg.com:8577/"
# ============================================================================
# MQTT topics
# ----------------------------------------------------------------------------
# Telemetry is published under "<rootTopic>/...". Commands are published to
# "<rootTopic>/<path>/control". rootTopic comes from /user_spa_details/.
# ============================================================================
STATUS_WILDCARD = "{root}/#"
CONTROL_TOPIC = "{root}/{path}/control"
# ============================================================================
# Spa capability ranges
# ============================================================================
# FreshWater salt system output level; the app coerces the value to 0..10.
SALT_LEVEL_MIN = 0
SALT_LEVEL_MAX = 10
# Jets commanded on/off. JET1 is dual-speed, JET2 single-speed; some models add JET3.
JETS = ("jet1", "jet2")
# ============================================================================
# Home Assistant integration constants
# ============================================================================
DOMAIN = "hotspring"
MANUFACTURER = "Watkins Wellness"
CONF_EMAIL = "email"
CONF_PASSWORD = "password" # noqa: S105 - config-flow field name, not a secret
CONF_CERT_DIR = "cert_dir"
# Optional: the dongle's local ``/status`` URL (e.g. http://<spa-ip>/status).
# When set, telemetry is polled locally instead of subscribed over the cloud MQTT
# broker, so monitoring stays on-LAN and off the vendor cloud. Control still uses
# the cloud MQTT connection. Only reachable when HA is on the spa's LAN.
CONF_LOCAL_STATUS_URL = "local_status_url"
LOCAL_POLL_INTERVAL = 30 # seconds
# The vendor mutual-TLS client certificate is NOT shipped in this repo (it is
# Watkins material extractable from the app APK). Place the three PEM files here
# on the Home Assistant host; see the README.
DEFAULT_CERT_DIR = "/config/hotspring/certs"
CERT_FILE = "client1_cert.pem"
KEY_FILE = "client1_key.pem"
CA_FILE = "ca_cert.pem"