2023-11-03 00:13:55 +02:00
|
|
|
extends Node
|
|
|
|
|
2023-11-24 02:36:31 +02:00
|
|
|
signal on_connect()
|
|
|
|
signal on_disconnect()
|
|
|
|
var connected := false
|
|
|
|
|
|
|
|
var devices_template := FileAccess.get_file_as_string("res://lib/home_apis/hass_ws/templates/devices.j2")
|
2023-11-03 03:07:53 +02:00
|
|
|
var socket := WebSocketPeer.new()
|
2023-11-03 00:13:55 +02:00
|
|
|
# in seconds
|
2023-11-03 03:07:53 +02:00
|
|
|
var request_timeout := 10.0
|
2023-11-03 00:13:55 +02:00
|
|
|
|
2023-11-14 00:25:59 +02:00
|
|
|
# var url := "wss://8ybjhqcinfcdyvzu.myfritz.net:8123/api/websocket"
|
2023-11-21 12:40:06 +02:00
|
|
|
var url := ""
|
|
|
|
var token := ""
|
|
|
|
|
2023-11-14 00:25:59 +02:00
|
|
|
|
2023-11-05 22:32:50 +02:00
|
|
|
var LOG_MESSAGES := false
|
2023-11-03 00:13:55 +02:00
|
|
|
|
2023-11-03 03:07:53 +02:00
|
|
|
var authenticated := false
|
2023-11-24 02:36:31 +02:00
|
|
|
|
2023-11-03 03:07:53 +02:00
|
|
|
var id := 1
|
|
|
|
var entities: Dictionary = {}
|
|
|
|
var entitiy_callbacks := CallbackMap.new()
|
|
|
|
var packet_callbacks := CallbackMap.new()
|
|
|
|
|
2023-11-03 00:13:55 +02:00
|
|
|
func _init(url := self.url, token := self.token):
|
|
|
|
self.url = url
|
|
|
|
self.token = token
|
|
|
|
|
2023-11-05 17:36:13 +02:00
|
|
|
devices_template = devices_template.replace("\n", " ").replace("\t", "").replace("\r", " ")
|
2023-11-03 00:13:55 +02:00
|
|
|
connect_ws()
|
|
|
|
|
|
|
|
func connect_ws():
|
2023-11-23 04:41:13 +02:00
|
|
|
if url == "" || token == "":
|
|
|
|
return
|
|
|
|
|
2023-11-03 00:13:55 +02:00
|
|
|
print("Connecting to %s" % self.url)
|
|
|
|
socket.connect_to_url(self.url)
|
2023-11-16 20:23:17 +02:00
|
|
|
set_process(true)
|
2023-11-03 00:13:55 +02:00
|
|
|
|
2023-11-03 23:00:05 +02:00
|
|
|
# https://github.com/godotengine/godot/issues/84423
|
|
|
|
# Otherwise the WebSocketPeer will crash when receiving large packets
|
2023-12-13 23:22:52 +02:00
|
|
|
socket.set_inbound_buffer_size(65535 * 4)
|
2023-11-03 23:00:05 +02:00
|
|
|
|
2023-11-03 00:13:55 +02:00
|
|
|
func _process(delta):
|
|
|
|
socket.poll()
|
2023-11-03 03:07:53 +02:00
|
|
|
|
2023-11-03 00:13:55 +02:00
|
|
|
var state = socket.get_ready_state()
|
|
|
|
if state == WebSocketPeer.STATE_OPEN:
|
|
|
|
while socket.get_available_packet_count():
|
2023-11-03 23:00:05 +02:00
|
|
|
var packet = decode_packet(socket.get_packet())
|
|
|
|
if typeof(packet) == TYPE_DICTIONARY:
|
|
|
|
handle_packet(packet)
|
|
|
|
elif typeof(packet) == TYPE_ARRAY:
|
|
|
|
for p in packet:
|
|
|
|
handle_packet(p)
|
2023-11-03 00:13:55 +02:00
|
|
|
elif state == WebSocketPeer.STATE_CLOSING:
|
|
|
|
pass
|
|
|
|
elif state == WebSocketPeer.STATE_CLOSED:
|
|
|
|
var code = socket.get_close_code()
|
|
|
|
var reason = socket.get_close_reason()
|
2023-12-13 23:22:52 +02:00
|
|
|
|
|
|
|
if reason == "":
|
|
|
|
reason = "Invalid URL"
|
|
|
|
|
|
|
|
var message = "WS connection closed with code: %s, reason: %s" % [code, reason]
|
|
|
|
EventSystem.notify(message, EventNotify.Type.DANGER)
|
|
|
|
print(message)
|
2023-11-03 03:07:53 +02:00
|
|
|
handle_disconnect()
|
2023-11-03 00:13:55 +02:00
|
|
|
|
2023-11-03 23:00:05 +02:00
|
|
|
func handle_packet(packet: Dictionary):
|
2023-11-05 17:36:13 +02:00
|
|
|
if LOG_MESSAGES: print("Received packet: %s" % str(packet).substr(0, 1000))
|
2023-11-03 00:13:55 +02:00
|
|
|
|
|
|
|
if packet.type == "auth_required":
|
|
|
|
send_packet({
|
|
|
|
"type": "auth",
|
|
|
|
"access_token": self.token
|
|
|
|
})
|
|
|
|
|
|
|
|
elif packet.type == "auth_ok":
|
|
|
|
authenticated = true
|
2023-11-03 03:07:53 +02:00
|
|
|
start_subscriptions()
|
|
|
|
|
2023-11-03 00:13:55 +02:00
|
|
|
elif packet.type == "auth_invalid":
|
2023-12-13 23:22:52 +02:00
|
|
|
EventSystem.notify("Failed to authenticate, invalid auth token", EventNotify.Type.DANGER)
|
|
|
|
print("Failed to authenticate, invalid auth token")
|
2023-11-03 03:07:53 +02:00
|
|
|
handle_disconnect()
|
2023-11-03 00:13:55 +02:00
|
|
|
else:
|
2023-11-03 23:00:05 +02:00
|
|
|
packet_callbacks.call_key(int(packet.id), [packet])
|
2023-11-03 03:07:53 +02:00
|
|
|
|
|
|
|
func start_subscriptions():
|
|
|
|
assert(authenticated, "Not authenticated")
|
|
|
|
|
2023-11-03 23:00:05 +02:00
|
|
|
# await send_request_packet({
|
|
|
|
# "type": "supported_features",
|
|
|
|
# "features": {
|
|
|
|
# "coalesce_messages": 1
|
|
|
|
# }
|
|
|
|
# })
|
|
|
|
|
|
|
|
# await send_request_packet({
|
|
|
|
# "type": "subscribe_events",
|
|
|
|
# "event_type": "state_changed"
|
|
|
|
# })
|
2023-11-03 03:07:53 +02:00
|
|
|
|
|
|
|
send_subscribe_packet({
|
|
|
|
"type": "subscribe_entities"
|
|
|
|
}, func(packet: Dictionary):
|
|
|
|
if packet.type != "event":
|
|
|
|
return
|
|
|
|
|
|
|
|
if packet.event.has("a"):
|
|
|
|
for entity in packet.event.a.keys():
|
2023-11-03 23:00:05 +02:00
|
|
|
entities[entity] = {
|
|
|
|
"state": packet.event.a[entity]["s"],
|
|
|
|
"attributes": packet.event.a[entity]["a"]
|
|
|
|
}
|
2023-11-03 03:07:53 +02:00
|
|
|
entitiy_callbacks.call_key(entity, [entities[entity]])
|
2023-11-24 02:36:31 +02:00
|
|
|
connected = true
|
2023-11-03 03:07:53 +02:00
|
|
|
on_connect.emit()
|
|
|
|
|
|
|
|
if packet.event.has("c"):
|
|
|
|
for entity in packet.event.c.keys():
|
2023-11-03 23:00:05 +02:00
|
|
|
if !entities.has(entity):
|
|
|
|
continue
|
|
|
|
|
2023-11-03 03:07:53 +02:00
|
|
|
if packet.event.c[entity].has("+"):
|
2023-11-03 23:00:05 +02:00
|
|
|
if packet.event.c[entity]["+"].has("s"):
|
|
|
|
entities[entity]["state"] = packet.event.c[entity]["+"]["s"]
|
|
|
|
if packet.event.c[entity]["+"].has("a"):
|
2023-11-12 18:36:23 +02:00
|
|
|
entities[entity]["attributes"].merge(packet.event.c[entity]["+"]["a"], true)
|
2023-11-03 03:07:53 +02:00
|
|
|
entitiy_callbacks.call_key(entity, [entities[entity]])
|
|
|
|
)
|
|
|
|
|
|
|
|
func handle_disconnect():
|
|
|
|
authenticated = false
|
|
|
|
set_process(false)
|
|
|
|
on_disconnect.emit()
|
|
|
|
|
|
|
|
func send_subscribe_packet(packet: Dictionary, callback: Callable):
|
|
|
|
packet.id = id
|
|
|
|
id += 1
|
|
|
|
|
|
|
|
packet_callbacks.add(packet.id, callback)
|
2023-11-03 23:00:05 +02:00
|
|
|
send_packet(packet)
|
2023-11-03 03:07:53 +02:00
|
|
|
|
|
|
|
return func():
|
|
|
|
packet_callbacks.remove(packet.id, callback)
|
|
|
|
send_packet({
|
|
|
|
id: id,
|
|
|
|
"type": packet.type.replace("subscribe", "unsubscribe"),
|
|
|
|
"subscription": packet.id
|
|
|
|
})
|
|
|
|
id += 1
|
|
|
|
|
2023-11-03 00:13:55 +02:00
|
|
|
|
2023-11-05 17:36:13 +02:00
|
|
|
func send_request_packet(packet: Dictionary, ignore_initial := false):
|
2023-11-03 00:13:55 +02:00
|
|
|
packet.id = id
|
|
|
|
id += 1
|
|
|
|
|
|
|
|
send_packet(packet)
|
|
|
|
|
2023-11-05 17:36:13 +02:00
|
|
|
var promise = Promise.new(func(resolve: Callable, reject: Callable):
|
|
|
|
var fn: Callable
|
|
|
|
|
|
|
|
if ignore_initial:
|
|
|
|
fn = func(packet: Dictionary):
|
|
|
|
if packet.type == "event":
|
|
|
|
resolve.call(packet)
|
|
|
|
packet_callbacks.remove(packet.id, fn)
|
|
|
|
|
|
|
|
packet_callbacks.add(packet.id, fn)
|
|
|
|
else:
|
|
|
|
packet_callbacks.add_once(packet.id, resolve)
|
2023-11-03 00:13:55 +02:00
|
|
|
|
|
|
|
var timeout = Timer.new()
|
|
|
|
timeout.set_wait_time(request_timeout)
|
|
|
|
timeout.set_one_shot(true)
|
|
|
|
timeout.timeout.connect(func():
|
|
|
|
reject.call(Promise.Rejection.new("Request timed out"))
|
2023-11-05 17:36:13 +02:00
|
|
|
if ignore_initial:
|
|
|
|
packet_callbacks.remove(packet.id, fn)
|
|
|
|
else:
|
|
|
|
packet_callbacks.remove(packet.id, resolve)
|
2023-11-03 00:13:55 +02:00
|
|
|
)
|
|
|
|
add_child(timeout)
|
|
|
|
timeout.start()
|
|
|
|
)
|
|
|
|
|
|
|
|
return await promise.settled
|
|
|
|
|
|
|
|
|
|
|
|
func send_packet(packet: Dictionary):
|
2023-11-05 17:36:13 +02:00
|
|
|
if LOG_MESSAGES: print("Sending packet: %s" % encode_packet(packet))
|
2023-11-03 00:13:55 +02:00
|
|
|
socket.send_text(encode_packet(packet))
|
|
|
|
|
|
|
|
func decode_packet(packet: PackedByteArray):
|
|
|
|
return JSON.parse_string(packet.get_string_from_utf8())
|
|
|
|
|
|
|
|
func encode_packet(packet: Dictionary):
|
|
|
|
return JSON.stringify(packet)
|
|
|
|
|
2023-11-24 02:36:31 +02:00
|
|
|
func has_connected():
|
|
|
|
return connected
|
2023-11-03 23:00:05 +02:00
|
|
|
|
2023-11-24 02:36:31 +02:00
|
|
|
func get_devices():
|
2023-11-05 17:36:13 +02:00
|
|
|
var result = await send_request_packet({
|
|
|
|
"type": "render_template",
|
|
|
|
"template": devices_template,
|
|
|
|
"timeout": 3,
|
|
|
|
"report_errors": true
|
|
|
|
}, true)
|
|
|
|
|
|
|
|
return result.payload.event.result
|
|
|
|
|
|
|
|
func get_device(id: String):
|
|
|
|
pass
|
2023-11-03 00:13:55 +02:00
|
|
|
|
|
|
|
func get_state(entity: String):
|
2023-11-03 03:07:53 +02:00
|
|
|
if entities.has(entity):
|
|
|
|
return entities[entity]
|
2023-11-03 23:00:05 +02:00
|
|
|
return null
|
2023-11-03 00:13:55 +02:00
|
|
|
|
|
|
|
|
|
|
|
func watch_state(entity: String, callback: Callable):
|
2023-11-03 03:07:53 +02:00
|
|
|
entitiy_callbacks.add(entity, callback)
|
2023-11-03 00:13:55 +02:00
|
|
|
|
2023-11-05 17:36:13 +02:00
|
|
|
return func():
|
|
|
|
entitiy_callbacks.remove(entity, callback)
|
|
|
|
|
2023-11-03 00:13:55 +02:00
|
|
|
|
|
|
|
func set_state(entity: String, state: String, attributes: Dictionary = {}):
|
2023-11-03 03:07:53 +02:00
|
|
|
var domain = entity.split(".")[0]
|
2023-11-03 23:00:05 +02:00
|
|
|
var service: String
|
|
|
|
|
|
|
|
if domain == 'switch':
|
|
|
|
if state == 'on':
|
|
|
|
service = 'turn_on'
|
|
|
|
elif state == 'off':
|
|
|
|
service = 'turn_off'
|
|
|
|
elif domain == 'light':
|
|
|
|
if state == 'on':
|
|
|
|
service = 'turn_on'
|
|
|
|
elif state == 'off':
|
|
|
|
service = 'turn_off'
|
2023-11-26 03:01:27 +02:00
|
|
|
elif domain == 'media_player':
|
|
|
|
if state == 'play':
|
|
|
|
service = 'media_play'
|
|
|
|
elif state == "pause":
|
|
|
|
service = "media_pause"
|
|
|
|
elif state == "next":
|
|
|
|
service = "media_next_track"
|
|
|
|
elif state == "previous":
|
|
|
|
service = "media_previous_track"
|
2023-11-29 01:47:11 +02:00
|
|
|
elif state == "volume":
|
|
|
|
service = "volume_set"
|
2023-12-10 01:34:54 +02:00
|
|
|
elif domain == 'button':
|
|
|
|
if state == 'pressed':
|
|
|
|
service = 'press'
|
2023-11-03 03:07:53 +02:00
|
|
|
|
2023-11-05 17:36:13 +02:00
|
|
|
if service == null:
|
|
|
|
return null
|
|
|
|
|
2023-11-03 03:07:53 +02:00
|
|
|
return await send_request_packet({
|
|
|
|
"type": "call_service",
|
|
|
|
"domain": domain,
|
|
|
|
"service": service,
|
|
|
|
"service_data": attributes,
|
|
|
|
"target": {
|
|
|
|
"entity_id": entity
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2023-11-03 00:13:55 +02:00
|
|
|
|