Mac » UR12 » アクティブスピーカーな構成なのですが、Macのシステム終了時に、先にアクティブスピーカーの電源を落としておかないと、ボンという大きい音がなってしまいます。精神的にもスピーカー的にもよろしくないので、何とかならないかと思っていたところ、以前購入していたSwitchbotのプラグミニがBLE(Bluetooth Low Energy)でコントロールできるとのことで、Core Bluetoothを使ってアプリを作成することに。
SwitchBot スマートプラグ プラグミニ HomeKit対応 2個入りをAmazonで見る
まずは公式サイトにてAPIを確認。
電源オンは0x570f50010180を
電源オフは0x570f50010100をそれぞれ送れば良いらしい。
- Turn On Plug Mini REQ 0x570f50010180 RESP 0x0180
- Turn Off Plug Mini REQ 0x570f50010100 RESP 0x0100
Core Bluetoothおよびプロセつについては、ググって以下を使わせていただきました。
SwiftでBluetooth通信をマスター!たったの10ステップで完璧に実装 – Japanシーモア
[macos]管理者でプロセス実行 #Swift - Qiita
最初は1つのアプリで電源オンとオフを実現しようとしたけれど、システム終了時の処理がどうやってもうまくいかず、2つのアプリに分けることに。
まずは、お使いのプラグミニのUUIDを調べてください。
プラグミニのUUIDの検索方法
私は、LightBlueというアプリを使いました。
カテゴリ: Utilities
価格: 無料
プラグミニの電源を入れたら、アプリを起動してください。
検索を始めます。
検索結果にWoPlugという名前が表示されたら、それがプラグミニのものになります。
(プラグミニを複数設置している場合は、設定するもの以外を外した方が確実です。)
私の場合は、最初なかなか表示されず、プラグミニの設置位置を色々変えたりしていたら、ようやく表示されました。
右のConnectをクリックしてください。
接続が完了すると、以下のように表示されます。
このUUID部分がお使いのプラグミニのUUIDになります。
アプリを作成していきます。
電源オンの方。Macの起動時に、プラグミニの電源を入れます。
Xcodeを起動したら、新規プロジェクトを作成。
新規プロジェクトはSwift、SwiftUIで、プロジェクト名:PlugminiPowerOnApp
新規ファイルを作成します。
ファイル名;BluetoothService.swiftを作成
以下をお使いのプラグミニのUUID置き換えてください。
var serviceUUID = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" //プラグミニのUUID
//
// BluetoothService.swift
//
import Foundation
import CoreBluetooth
import AppKit
class BluetoothService: NSObject, ObservableObject {
static var shared = BluetoothService()
@Published var isSearching: Bool = false
var centralManager: CBCentralManager!
var serviceUUID = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" //プラグミニのUUID
// 接続先の機器
private var connectPeripheral: CBPeripheral? = nil
override init() {
super.init()
self.centralManager = CBCentralManager()
}
// MARK: - Methods
/// スキャン開始
func startBluetoothScan() {
print("スキャン開始")
self.centralManager = CBCentralManager(delegate: self, queue: nil)
}
/// スキャン停止
func stopBluetoothScan() {
print("スキャン停止")
self.centralManager.stopScan()
self.isSearching = false
}
}
// MARK: CBCentralManagerDelegate
extension BluetoothService: CBCentralManagerDelegate, CBPeripheralDelegate {
// Bluetoothのステータスを取得する(CBCentralManagerの状態が変わる度に呼び出される)
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .poweredOff:
print("Bluetooth PoweredOff")
break
case .poweredOn:
print("Bluetooth poweredOn")
self.centralManager.scanForPeripherals(withServices: nil, options: nil)
self.isSearching = true
break
case .resetting:
print("Bluetooth resetting")
break
case .unauthorized:
print("Bluetooth unauthorized")
break
case .unknown:
print("Bluetooth unknown")
break
case .unsupported:
print("Bluetooth unsupported")
break
@unknown default:
print("unknown")
}
}
// スキャン結果取得
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
// 対象機器のみ保持する
print("Name=\(String(describing: peripheral.name))")
print("services=\(String(describing: peripheral.services))")
print("UUID=\(peripheral.identifier.uuidString)")
if peripheral.identifier.uuidString == serviceUUID {
// 対象機器のみ保持する
self.connectPeripheral = peripheral
// 機器に接続
print("機器に接続:\(String(describing: peripheral.name))")
self.centralManager.connect(peripheral, options: nil)
//self.stopBluetoothScan() //ここで止めるとエラーになる
}
}
// 接続成功時
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
print("接続成功")
self.connectPeripheral = peripheral
self.connectPeripheral?.delegate = self
//ペリフェラルのサービスを探索
if let peripheral = self.connectPeripheral {
//
peripheral.discoverServices(nil)
}
// スキャン停止処理
self.stopBluetoothScan()
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
guard let services = peripheral.services else { return }
for service in services {
print("Peripheralのサービスが見つかったのでキャラクタリスティックを検索")
// キャラクタリスティックを検索
peripheral.discoverCharacteristics(nil, for: service)
}
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
for characteristic in service.characteristics! {
print("didDiscoverCharacteristicsFor")
if characteristic.properties.contains(.write) {
print("書き込み可能なキャラクタリスティックが見つかった")
//書き込むデータを作成
//電源オン
print("電源オン")
let bytes : [UInt8] = [ 0x57, 0x0F, 0x50, 0x01, 0x01, 0x80]
let data = Data(bytes)
// キャラクタリスティックからデータを書き込む
peripheral.writeValue(data, for: characteristic, type: .withResponse)
//ペリフェラルデバイスとの接続 ここでやると正常に書き込めない
//self.disconnectPeripheral(central: self.centralManager, peripheral: peripheral)
}
}
}
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
print("didWriteValueFor")
//ペリフェラルデバイスとの接続を切る
self.disconnectPeripheral(central: self.centralManager, peripheral: peripheral)
//終了時ならば自分を終了する
print("正常に書き込まれたので自分自身を終了")
NSApplication.shared.terminate(self)
}
// 接続失敗時
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
print("接続失敗:\(String(describing: error))")
// スキャン停止処理
self.stopBluetoothScan()
}
// ペリフェラルデバイスとの接続を切断する関数
func disconnectPeripheral(central: CBCentralManager, peripheral: CBPeripheral) {
central.cancelPeripheralConnection(peripheral)
}
// 接続切断時
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
//print("接続切断:\(String(describing: error))")
if let error = error {
// エラー情報を出力
print("切断時にエラーが発生しました: \(error.localizedDescription)")
} else {
// 通常の切断処理
print("正常に切断されました。")
}
}
}
これを、PlugminiPowerOnApp.swift側では、以下のように記述
(内容は、PlugminiPowerOnApp、PlugminiPowerOffApp共通です。)
import SwiftUI
@main
struct PlugminiPowerOnApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
ContentView()
}
.windowResizability(.contentSize)
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
@ObservedObject private var bluetoothService = BluetoothService.shared
// MARK: - AppDelegate Lifecycle
func applicationWillFinishLaunching(_ aNotification: Notification) {
print("applicationWillFinishLaunching")
//起動時の処理
}
func applicationDidFinishLaunching(_ aNotification: Notification) {
print("applicationDidFinishLaunching")
//プラグインミニの電源を入れる(PlugminiPowerOffの場合は切る)
self.bluetoothService.isSearching = true
self.bluetoothService.startBluetoothScan()
}
// アプリケーション終了直前通知 applicationShouldTerminateAfterLastWindowClosedの後、applicationWillTerminate よりも先
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
print("applicationShouldTerminate")
return NSApplication.TerminateReply.terminateNow
}
func applicationWillTerminate(_ notification: Notification) {
print("applicationWillTerminate")
}
}
TARGETのInfo.plistに以下を追加。Stringに記述する説明文は任意のものを
Privacy - Bluetooth Always Usage Description
Privacy - Bluetooth Peripheral Usage Description
Sandboxを有効にしているのなら、Signing & Capabilities » App Sandbox »HardwareにてBluetoothにチェックを入れること。
出来上がったアプリは、Sleepyhead.appに登録して、少し遅らせて起動して使っています。
次は電源オフの方。プラグミニの電源を切って、システムを終了します。
Xcodeを起動したら、新規プロジェクトを作成。
新規プロジェクトはSwift、SwiftUIで、プロジェクト名:PlugminiPowerOffApp
SandboxをNOに(電源オフ後にシステムを終了させるため)
以下の2箇所をそれそれ置き換えてください。
var serviceUUID = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" //プラグミニのUUID
let password = "システム管理者パスワード"
//
// BluetoothService.swift
// PlugminiPowerOffApp
//
import Foundation
import CoreBluetooth
import AppKit
class BluetoothService: NSObject, ObservableObject {
static var shared = BluetoothService()
@Published var isSearching: Bool = false
var centralManager: CBCentralManager!
var serviceUUID = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" //プラグミニのUUID
/// 接続先の機器
private var connectPeripheral: CBPeripheral? = nil
override init() {
super.init()
self.centralManager = CBCentralManager()
}
// MARK: - Public Methods
/// スキャン開始
func startBluetoothScan() {
print("スキャン開始")
self.centralManager = CBCentralManager(delegate: self, queue: nil)
}
/// スキャン停止
func stopBluetoothScan() {
print("スキャン停止")
self.centralManager.stopScan()
self.isSearching = false
}
}
// MARK: CBCentralManagerDelegate
extension BluetoothService: CBCentralManagerDelegate, CBPeripheralDelegate {
// Bluetoothのステータスを取得する(CBCentralManagerの状態が変わる度に呼び出される)
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .poweredOff:
print("Bluetooth PoweredOff")
break
case .poweredOn:
print("Bluetooth poweredOn")
self.centralManager.scanForPeripherals(withServices: nil, options: nil)
//self.centralManager.scanForPeripherals(withServices: serviceUUID, options: nil)
self.isSearching = true
break
case .resetting:
print("Bluetooth resetting")
break
case .unauthorized:
print("Bluetooth unauthorized")
break
case .unknown:
print("Bluetooth unknown")
break
case .unsupported:
print("Bluetooth unsupported")
break
@unknown default:
print("unknown")
}
}
// スキャン結果取得
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
// 対象機器のみ保持する
print("Name=\(String(describing: peripheral.name))")
print("services=\(String(describing: peripheral.services))")
print("UUID=\(peripheral.identifier.uuidString)")
if peripheral.identifier.uuidString == serviceUUID {
// 対象機器のみ保持する
self.connectPeripheral = peripheral
// 機器に接続
print("機器に接続:\(String(describing: peripheral.name))")
self.centralManager.connect(peripheral, options: nil)
//self.stopBluetoothScan() //ここで止めるとエラーになる
}
}
// 接続成功時
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
print("接続成功")
self.connectPeripheral = peripheral
self.connectPeripheral?.delegate = self
//ペリフェラルのサービスを探索
if let peripheral = self.connectPeripheral {
peripheral.discoverServices(nil)
}
// スキャン停止処理
self.stopBluetoothScan()
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
guard let services = peripheral.services else { return }
for service in services {
print("Peripheralのサービスが見つかったのでキャラクタリスティックを検索")
// キャラクタリスティックを検索
peripheral.discoverCharacteristics(nil, for: service)
}
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
for characteristic in service.characteristics! {
print("didDiscoverCharacteristicsFor")
if characteristic.properties.contains(.write) {
print("書き込み可能なキャラクタリスティックが見つかった")
//書き込むデータを作成
//電源オフ
//Turn Off Plug Mini REQ 0x570f50010100 RESP 0x0100
print("電源オフ")
let bytes : [UInt8] = [ 0x57, 0x0F, 0x50, 0x01, 0x01, 0x00]
let data = Data(bytes)
// キャラクタリスティックからデータを書き込む
peripheral.writeValue(data, for: characteristic, type: .withResponse)
//ペリフェラルデバイスとの接続 ここでやると正常に書き込めない
//self.disconnectPeripheral(central: self.centralManager, peripheral: peripheral)
}
}
}
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
print("didWriteValueFor")
//ペリフェラルデバイスとの接続を切る
self.disconnectPeripheral(central: self.centralManager, peripheral: peripheral)
print("正常に書き込まれたのでシステム終了")
//10秒後にシステム終了を実行
self.shutdownProcess()
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
//3秒後に自分自身を終了
NSApplication.shared.terminate(self)
}
}
func shutdownProcess() {
let process = Process()
// -S: パスワードを端末ではなく標準入力から読み込む
process.launchPath = "/usr/bin/sudo"
////sudo bash -c 'sleep 10 && shutdown -h now'
let command = "sleep 10 && shutdown -h now"
process.arguments = ["-S","bash", "-c", command]
// パイプを設定
let stdin = Pipe()
let stdout = Pipe()
process.standardOutput = stdout
process.standardError = stdout
process.standardInput = stdin
// 実行
process.launch()
// パスワードの末尾には改行が必要
let password = "システム管理者パスワード"
let passwordWithNewline = password + "\n"
// パスワードを標準入力へ書き込み
stdin.fileHandleForWriting.write(passwordWithNewline.data(using: .utf8)!)
try? stdin.fileHandleForWriting.close()
// 終了まで待機(システム終了するので必要ない)
/*
process.waitUntilExit()
// 標準出力を取得
if let data = try? stdout.fileHandleForReading.readToEnd(),
let output = String(data: data, encoding: .utf8) {
print(output)
}*/
}
// 接続失敗時
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
print("接続失敗:\(String(describing: error))")
// スキャン停止処理
self.stopBluetoothScan()
}
// ペリフェラルデバイスとの接続を切断する関数
func disconnectPeripheral(central: CBCentralManager, peripheral: CBPeripheral) {
central.cancelPeripheralConnection(peripheral)
}
// 接続切断時
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
//print("接続切断:\(String(describing: error))")
if let error = error {
// エラー情報を出力
print("切断時にエラーが発生しました: \(error.localizedDescription)")
} else {
// 通常の切断処理
print("正常に切断されました。")
}
}
}
システム終了は、アップルメニューから行うのではなく、このアプリを起動して行います。
私は、このアプリをショートカット.appで、アクションを作成して、メニューバーに固定して使っています。
名前は分かりやすいものに変更しています。