Menu-bar utility that watches your Mac's battery level and toggles a smart plug on or off to keep the charge within a configurable window. The provider architecture is extensible — Tuya is the only supported provider today, but adding new brands (Meross, Kasa, etc.) requires implementing a single protocol.
MacSwitApplaunches, wires anAppDelegatefor shutdown handling, and creates a sharedAppState. AMenuBarExtrashows a plug icon (filled when the plug is on).AppStateloads persisted thresholds, polling interval, and preferences via@AppStorage. It asksPlugStorefor the active plug configuration and builds a provider controller throughPlugProviderFactory.- A repeating
Timer(minimum 60 s) callsperformCheck, which runsBatteryReaderoff the main thread to read the current percentage via IOKit (falling back topmset -g batt). evaluateBatterycompares the reading to the on/off thresholds, validateson < off, and deduplicates commands — the same action is never sent twice until the opposite action occurs.- When an action is needed, the active
PlugProvidingcontroller sends the command. For Tuya,TuyaPlugControllerdelegates toTuyaClient— an actor that lazily fetches a token via/v1.0/token, caches it until one minute before expiry, verifies the device is online, and signs REST calls with HMAC-SHA256 before hitting/v1.0/iot-03/devices/{deviceId}/commands. PlugStoremanages multiple plug configurations persisted inUserDefaults, with credentials (Access ID and Access Secret) stored per-plug in the login keychain.- On quit,
AppDelegateoptionally sends a switch-off command (with a 3-second timeout) if the user has enabled Switch off on shutdown.
MacSwit/
├── MacSwitApp.swift # App entry point, MenuBarExtra + Settings scene
├── AppDelegate.swift # Shutdown handler (switch-off on quit)
├── State/
│ ├── AppState.swift # Central state: timer, battery check, plug control
│ └── AppSettings.swift # AppStorage keys and default constants
├── Models/
│ ├── PlugConfig.swift # Per-plug configuration (name, provider, fields)
│ └── TuyaEndpoint.swift # Tuya region endpoints
├── Services/
│ ├── BatteryReader.swift # IOKit / pmset battery reading
│ ├── KeychainStore.swift # Login keychain wrapper
│ ├── PlugStore.swift # Multi-plug CRUD + active selection
│ └── TuyaClient.swift # Tuya REST API actor (token, signing, commands)
├── Providers/
│ ├── SmartPlugProvider.swift # PlugProviding protocol, ProviderType, PlugProviderFactory
│ └── Tuya/
│ ├── TuyaPlugController.swift # PlugProviding implementation for Tuya
│ └── TuyaPlugFieldsView.swift # Tuya-specific settings form fields
├── Views/
│ ├── MenuView.swift # Menu-bar popup (battery, status, enable/disable, quit)
│ ├── SettingsView.swift # Settings window (Battery, Smart Plug, General tabs)
│ └── PlugEditView.swift # Add/edit plug sheet (credentials, provider fields, test)
├── Helpers/
│ └── CryptoHelpers.swift # SHA-256 and HMAC-SHA256 utilities
└── landing/
└── index.html # Product landing page (dark/light mode, EN/TR)
- macOS 13.0+ (Menu Bar Extra + login-item APIs). Login-item support requires a signed
.appbundle. - Xcode 15+ / Swift 5.9 toolchain.
- A Tuya developer account with a cloud project and a device already paired to that project.
- Tuya API credentials: Access ID, Access Secret, region endpoint (EU/US/CN or custom), Device ID, and optional DP code (defaults to
switch_1).
- Open
MacSwit.xcodeprojin Xcode and select the MacSwit scheme. - Set a signing team so the app can request login-item privileges.
- Build & run. The app appears in the menu bar as a plug icon.
-
Open Settings from the menu-bar popup.
-
Battery tab — set the lower/upper thresholds (plug turns ON at/below lower, OFF at/above upper). The sliders enforce a 5 % gap.
-
Smart Plug tab — manage your plugs. Click Add Plug to configure a new device, or edit/delete existing ones. One plug is active at a time; the active plug is used for automatic control.
-
In the plug editor, select the provider (currently Tuya), pick a region endpoint or enter a custom host, fill in Access ID, Access Secret, Device ID, and an optional DP code. Credentials are stored in the login keychain.
-
Use Test ON / Test OFF to verify the relay reacts, and Verify Token to confirm authentication works before relying on automation.
-
General tab — toggle Enable MacSwit, Launch at login, and the experimental Switch off on shutdown option.
Follow these steps to collect the values the plug editor requires:
- Create a cloud project. Sign in to the Tuya IoT Platform, go to Cloud → Development, and create a project in the same data center (EU/US/CN/other) as your hardware. Under Authorization Management, enable at least Device Status, Device Control, and Token Service APIs.
- Link the mobile app. In the project's Link Tuya App section, scan the QR code with the Smart Life or Tuya Smart app that controls the plug.
- Access ID & Access Secret. In the project dashboard under Authorization Key (or Project Configuration), copy the Access ID and Access Secret.
- Device ID. Pair the plug in Smart Life/Tuya Smart first. Then in the IoT Platform go to Devices → All Devices and copy the device's 20+ character ID.
- DP code. Open the device detail page, choose Standard Instruction Set (Functions). Most single-gang plugs use
switch_1; multi-gang plugs exposeswitch_2, etc. - Endpoint / region host. Match the host to your project's data center:
Data center Host China (Shanghai, Alibaba) openapi.tuyacn.comWestern America (Oregon, AWS) openapi.tuyaus.comEastern America (Virginia, Azure) openapi-ueaz.tuyaus.comCentral Europe (Frankfurt, AWS) openapi.tuyaeu.comWestern Europe (Amsterdam, Azure) openapi-weaz.tuyaeu.comIndia (Mumbai, AWS) openapi.tuyain.comSingapore (Alibaba) openapi-sg.iotbing.comOther / private Choose Custom and enter the hostname - Verify via API Explorer (optional). Before switching to MacSwit, use the IoT Platform's API Explorer to test
GET /v1.0/token,GET /v1.0/iot-03/devices/{device_id}, andPOST /v1.0/iot-03/devices/{device_id}/commandswith{ "code": "switch_1", "value": true }. - Enter the values in MacSwit's plug editor, save, and run the built-in test buttons.
The codebase is designed for extensibility. To add a new smart plug brand:
- Add a case to
ProviderTypeinSmartPlugProvider.swift(e.g.case meross = "meross"). - Create
Providers/<Brand>/<Brand>PlugController.swiftconforming to thePlugProvidingprotocol. - Create
Providers/<Brand>/<Brand>PlugFieldsView.swiftwith the brand-specific settings form. - Add the case to
PlugProviderFactory.make(config:accessId:accessSecret:). - Add the case to
PlugEditView.providerFieldsView. - Add provider-specific fields to
PlugConfig.
- Battery readings use IOKit when available and fall back to
pmset -g batt. - Command deduplication ensures the same action (ON or OFF) is never sent twice — only a threshold crossing in the opposite direction triggers a new command.
- Tuya tokens are cached until ~1 minute before expiry; saving new credentials clears the cache automatically.
- Each plug's Access ID and Access Secret are stored in the login keychain under
MacSwit.plug.<uuid>.accessIdandMacSwit.plug.<uuid>.accessSecret.