Merge pull request #11 from Nitwel/testing

Update main to latest progress
This commit is contained in:
Nitwel 2023-11-04 18:40:10 +01:00 committed by GitHub
commit 9e2f46784d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 1840 additions and 430 deletions

12
.editorconfig Normal file
View File

@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = tab
indent_size = 4
end_of_line = crlf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

2
.gitattributes vendored
View File

@ -1,3 +1,3 @@
# Normalize EOL for all files that Git considers text files. # Normalize EOL for all files that Git considers text files.
* text=auto eol=lf * text=auto eol=lf
Assets/** filter=lfs diff=lfs merge=lfs -text assets/** filter=lfs diff=lfs merge=lfs -text

View File

@ -1,3 +1,94 @@
# Immersive Home ![logo](assets/banner.png)
Using Godot 4.1.2 # 🏠 Introduction
Immersive Home is project to bring Smart Home and Mixed Reality technologies together for an intuitive and immersive experience.
## Features
- **Fast and Intuitive control over IoT devices**
- **Live overview over your smart home**
- **Simple way for creating automations**
- **Comfortable way of consuming virtual media unobstructed**
- **Advanced automations based on position in room**
## Supported Devices
**Smart Home Platforms**
- [Home Assistant](https://www.home-assistant.io/)
**Mixed Reality Headsets**
- Meta Quest 2 / Pro / 3
# 🛠 Development
In order to contribute to this project, you need the following to be setup before you can start working:
- Godot 4.1.3 installed
## Fundamentals
Communication with the Smart Home Environment is done using the `HomeAdapters` global. Each environment is made up of devices and entities.
A device is a collection of different entities and entities can represent many different things in a smart home.
For example, the entity of name `lights.smart_lamp_1` would control the kitchen lamps while `state.smart_lamp_1_temp` would show the current temperature of the lamp.
### Home Adapters
The `HomeAdapters` global allows to communicate with different backends and offers a set of fundamental functions allowing communication with the Smart Home.
```python
Device {
"id": String,
"name": String,
"entities": Array[Entity]
}
Entity {
"state": String
"attributes": Dictionary
}
# Get a list of all devices
func get_devices() -> Signal[Array[Device]]
# Get a single device by id
func get_device(id: String) -> Signal[Device]
# Returns the current state of an entity.
func get_state(entity: String) -> Signal[Entity]
# Updates the state of the entity and returns the resulting state
func set_state(entity: String, state: String, attributes: Dictionary) -> Signal[Entity]
# Watches the state and each time it changes, calls the callback with the changed state, returns a function to stop watching the state
func watch_state(entity: String, callback: Callable[entity: Entity]) -> Callable
```
### Interaction Events
Each time a button is pressed on the primary controller, a raycast is done to be able to interact with devices or the UI.
**InteractionEvent**
```js
{
position: Vector3,
rotation: Vector3
}
```
| Function called | Args | Description |
| -- | -- | -- |
| `_click` | `[event: InteractionEvent]` | The back trigger button has been pressed and released |
| `_dbl_click` | `[event: InteractionEvent]` | The back trigger button has been pressed and released twice in a row |
| `_long_click` | `[event: InteractionEvent]` | The back trigger button has been pressed, then hold still for a short period, then released |
| `_press_down` | `[event: InteractionEvent]` | The back trigger button has been pressed down |
| `_press_move` | `[event: InteractionEvent]` | The back trigger button has been moved while pressed down |
| `_press_up` | `[event: InteractionEvent]` | The back trigger button has been released |
| `_grab_down` | `[event: InteractionEvent]` | The side grap button been pressed down |
| `_grab_move` | `[event: InteractionEvent]` | The side grap button been pressed down |
| `_grab_up` | `[event: InteractionEvent]` | The side grap button been released |
### Testing without a VR Headset
In order to test without a headset, press the run project (F5) button in Godot and ignore the prompt that OpenXR failed to start.
To simulate the headset and controller movement, we're using the [XR Input Simulator](https://godotengine.org/asset-library/asset/1775) asset.
Click at the link to get a list of the supported controlls.

View File

@ -17,6 +17,8 @@ operator = 2
[sub_resource type="VisualShader" id="VisualShader_wb0u4"] [sub_resource type="VisualShader" id="VisualShader_wb0u4"]
code = "shader_type spatial; code = "shader_type spatial;
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_lambert, specular_schlick_ggx;
uniform vec4 Color : source_color; uniform vec4 Color : source_color;

View File

@ -102,7 +102,7 @@ function = 12
[resource] [resource]
code = "shader_type spatial; code = "shader_type spatial;
render_mode unshaded; render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_lambert, specular_schlick_ggx, unshaded;
uniform vec4 albedo : source_color; uniform vec4 albedo : source_color;
uniform float value; uniform float value;

View File

@ -63,7 +63,7 @@ condition = 1
[resource] [resource]
code = "shader_type spatial; code = "shader_type spatial;
render_mode unshaded; render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_lambert, specular_schlick_ggx, unshaded;
uniform vec4 bar_color : source_color; uniform vec4 bar_color : source_color;
uniform sampler2D bar_texture : source_color; uniform sampler2D bar_texture : source_color;

View File

@ -43,9 +43,9 @@ var webxr_auto_primary := 0
# Called when the node enters the scene tree for the first time. # Called when the node enters the scene tree for the first time.
func _ready(): func _ready():
var webxr_interface = XRServer.find_interface("WebXR") # var webxr_interface = XRServer.find_interface("WebXR")
if webxr_interface: # if webxr_interface:
XRServer.tracker_added.connect(self._on_webxr_tracker_added) # XRServer.tracker_added.connect(self._on_webxr_tracker_added)
_load() _load()

21
addons/promise/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 TheWalruzz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

165
addons/promise/promise.gd Normal file
View File

@ -0,0 +1,165 @@
extends RefCounted
class_name Promise
enum Status {
RESOLVED,
REJECTED
}
signal settled(status: PromiseResult)
signal resolved(value: Variant)
signal rejected(reason: Rejection)
## Generic rejection reason
const PROMISE_REJECTED := "Promise rejected"
var is_settled := false
func _init(callable: Callable):
resolved.connect(
func(value: Variant):
is_settled = true
settled.emit(PromiseResult.new(Status.RESOLVED, value)),
CONNECT_ONE_SHOT
)
rejected.connect(
func(rejection: Rejection):
is_settled = true
settled.emit(PromiseResult.new(Status.REJECTED, rejection)),
CONNECT_ONE_SHOT
)
callable.call_deferred(
func(value: Variant):
if not is_settled:
resolved.emit(value),
func(rejection: Rejection):
if not is_settled:
rejected.emit(rejection)
)
func then(resolved_callback: Callable) -> Promise:
resolved.connect(
resolved_callback,
CONNECT_ONE_SHOT
)
return self
func catch(rejected_callback: Callable) -> Promise:
rejected.connect(
rejected_callback,
CONNECT_ONE_SHOT
)
return self
static func from(input_signal: Signal) -> Promise:
return Promise.new(
func(resolve: Callable, _reject: Callable):
var number_of_args := input_signal.get_object().get_signal_list() \
.filter(func(signal_info: Dictionary) -> bool: return signal_info["name"] == input_signal.get_name()) \
.map(func(signal_info: Dictionary) -> int: return signal_info["args"].size()) \
.front() as int
if number_of_args == 0:
await input_signal
resolve.call(null)
else:
# only one arg in signal is allowed for now
var result = await input_signal
resolve.call(result)
)
static func from_many(input_signals: Array[Signal]) -> Array[Promise]:
return input_signals.map(
func(input_signal: Signal):
return Promise.from(input_signal)
)
static func all(promises: Array[Promise]) -> Promise:
return Promise.new(
func(resolve: Callable, reject: Callable):
var resolved_promises: Array[bool] = []
var results := []
results.resize(promises.size())
resolved_promises.resize(promises.size())
resolved_promises.fill(false)
for i in promises.size():
promises[i].then(
func(value: Variant):
results[i] = value
resolved_promises[i] = true
if resolved_promises.all(func(value: bool): return value):
resolve.call(results)
).catch(
func(rejection: Rejection):
reject.call(rejection)
)
)
static func any(promises: Array[Promise]) -> Promise:
return Promise.new(
func(resolve: Callable, reject: Callable):
var rejected_promises: Array[bool] = []
var rejections: Array[Rejection] = []
rejections.resize(promises.size())
rejected_promises.resize(promises.size())
rejected_promises.fill(false)
for i in promises.size():
promises[i].then(
func(value: Variant):
resolve.call(value)
).catch(
func(rejection: Rejection):
rejections[i] = rejection
rejected_promises[i] = true
if rejected_promises.all(func(value: bool): return value):
reject.call(PromiseAnyRejection.new(PROMISE_REJECTED, rejections))
)
)
class PromiseResult:
var status: Status
var payload: Variant
func _init(_status: Status, _payload: Variant):
status = _status
payload = _payload
class Rejection:
var reason: String
var stack: Array
func _init(_reason: String):
reason = _reason
stack = get_stack() if OS.is_debug_build() else []
func as_string() -> String:
return ("%s\n" % reason) + "\n".join(
stack.map(
func(dict: Dictionary) -> String:
return "At %s:%i:%s" % [dict["source"], dict["line"], dict["function"]]
))
class PromiseAnyRejection extends Rejection:
var group: Array[Rejection]
func _init(_reason: String, _group: Array[Rejection]):
super(_reason)
group = _group

View File

@ -0,0 +1,242 @@
extends Node
enum ControllerSelectionMode {Hold, Toggle}
@export var enabled: bool
@export var disable_xr_in_editor: bool = true
@export var controller_selection_mode: ControllerSelectionMode = ControllerSelectionMode.Hold
@export var device_x_sensitivity: float = 1
@export var device_y_sensitivity: float = 1
@export var scroll_sensitivity: float = 1
@export var is_camera_height_limited: bool = true
@export var min_camera_height: float = 0.5
@export var max_camera_height: float = 2.0
@export var xr_origin: NodePath
var origin: XROrigin3D
var camera: XRCamera3D
var left_controller: XRController3D
var right_controller: XRController3D
var left_tracker: XRPositionalTracker
var right_tracker: XRPositionalTracker
var toggle_left_controller = false
var toggle_right_controller = false
var toggle_shift = false
var key_map = {
KEY_1: "by_button",
KEY_2: "ax_button",
KEY_3: "by_touch",
KEY_4: "ax_touch",
KEY_5: "trigger_touch",
KEY_6: "grip_touch",
KEY_7: "secondary_click",
KEY_8: "secondary_touch",
KEY_9: "",
KEY_0: "",
KEY_MINUS: "primary_click",
KEY_EQUAL: "primary_touch",
KEY_BACKSPACE: "",
KEY_ENTER: "menu_button"
}
@onready var viewport: Viewport = get_viewport()
func _ready():
if not enabled or not OS.has_feature("editor"):
enabled = false
return
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
origin = get_node(xr_origin)
camera = origin.get_node("XRCamera3D")
var left_hand = XRServer.get_tracker("left_hand")
if left_hand == null:
left_tracker = XRPositionalTracker.new()
left_tracker.type = XRServer.TRACKER_CONTROLLER
left_tracker.hand = XRPositionalTracker.TRACKER_HAND_LEFT
left_tracker.name = "left_hand"
else:
left_tracker = left_hand
var right_hand = XRServer.get_tracker("right_hand")
if right_hand == null:
right_tracker = XRPositionalTracker.new()
right_tracker.type = XRServer.TRACKER_CONTROLLER
right_tracker.hand = XRPositionalTracker.TRACKER_HAND_RIGHT
right_tracker.name = "right_hand"
else:
right_tracker = right_hand
for child in origin.get_children():
if child.get("tracker"):
var pose = child.pose
if child.tracker == "left_hand":
left_controller = child
left_tracker.set_pose(pose, child.transform, Vector3.ZERO, Vector3.ZERO, XRPose.XR_TRACKING_CONFIDENCE_HIGH)
XRServer.add_tracker(left_tracker)
elif child.tracker == "right_hand":
right_controller = child
right_tracker.set_pose(pose, child.transform, Vector3.ZERO, Vector3.ZERO, XRPose.XR_TRACKING_CONFIDENCE_HIGH)
XRServer.add_tracker(right_tracker)
func _process(_delta):
if enabled and disable_xr_in_editor and OS.has_feature("editor") and viewport.use_xr:
viewport.use_xr = false
func _input(event):
if not enabled or not origin.current or not OS.has_feature("editor"):
return
if Input.is_key_pressed(KEY_ESCAPE):
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
elif Input.mouse_mode != Input.MOUSE_MODE_CAPTURED and event is InputEventMouseButton:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
if Input.mouse_mode != Input.MOUSE_MODE_CAPTURED:
return
simulate_joysticks()
var is_any_controller_selected = false
if event is InputEventMouseMotion:
if Input.is_physical_key_pressed(KEY_Q) or toggle_left_controller:
is_any_controller_selected = true
if Input.is_key_pressed(KEY_SHIFT):
rotate_device(event, left_controller)
else:
move_controller(event, left_controller)
if Input.is_physical_key_pressed(KEY_E) or toggle_right_controller:
is_any_controller_selected = true
if Input.is_key_pressed(KEY_SHIFT):
rotate_device(event, right_controller)
else:
move_controller(event, right_controller)
if not is_any_controller_selected:
rotate_device(event, camera)
elif event is InputEventMouseButton:
if Input.is_physical_key_pressed(KEY_Q) or toggle_left_controller:
is_any_controller_selected = true
attract_controller(event, left_controller)
simulate_trigger(event, left_controller)
simulate_grip(event, left_controller)
if Input.is_physical_key_pressed(KEY_E) or toggle_right_controller:
is_any_controller_selected = true
attract_controller(event, right_controller)
simulate_trigger(event, right_controller)
simulate_grip(event, right_controller)
if not is_any_controller_selected:
camera_height(event)
elif event is InputEventKey:
if controller_selection_mode == ControllerSelectionMode.Toggle and event.pressed:
if event.keycode == KEY_Q:
toggle_left_controller = !toggle_left_controller
elif event.keycode == KEY_E:
toggle_right_controller = !toggle_right_controller
if Input.is_physical_key_pressed(KEY_Q) or toggle_left_controller:
simulate_buttons(event, left_controller)
if Input.is_physical_key_pressed(KEY_E) or toggle_right_controller:
simulate_buttons(event, right_controller)
func camera_height(event: InputEventMouseButton):
var direction = -1
if not event.pressed:
return
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
direction = 1
elif event.button_index != MOUSE_BUTTON_WHEEL_DOWN:
return
var pos = camera.transform.origin
var camera_y = pos.y + (scroll_sensitivity * direction)/20
if (camera_y >= max_camera_height or camera_y <= min_camera_height) and is_camera_height_limited:
camera_y = pos.y
camera.transform.origin = Vector3(pos.x, camera_y , pos.z)
func simulate_joysticks():
var vec_left = vector_key_mapping(KEY_D, KEY_A, KEY_W, KEY_S)
left_tracker.set_input("primary", vec_left)
var vec_right = vector_key_mapping(KEY_RIGHT, KEY_LEFT, KEY_UP, KEY_DOWN)
right_tracker.set_input("primary", vec_right)
func simulate_trigger(event: InputEventMouseButton, controller: XRController3D):
if event.button_index == MOUSE_BUTTON_LEFT:
if controller.tracker == "left_hand":
left_tracker.set_input("trigger", float(event.pressed))
left_tracker.set_input("trigger_click", event.pressed)
else:
right_tracker.set_input("trigger", float(event.pressed))
right_tracker.set_input("trigger_click", event.pressed)
func simulate_grip(event: InputEventMouseButton, controller: XRController3D):
if event.button_index == MOUSE_BUTTON_RIGHT:
if controller.tracker == "left_hand":
left_tracker.set_input("grip", float(event.pressed))
left_tracker.set_input("grip_click", event.pressed)
else:
right_tracker.set_input("grip", float(event.pressed))
right_tracker.set_input("grip_click", event.pressed)
func simulate_buttons(event: InputEventKey, controller: XRController3D):
if key_map.has(event.keycode):
var button = key_map[event.keycode]
if controller.tracker == "left_hand":
left_tracker.set_input(button, event.pressed)
else:
right_tracker.set_input(button, event.pressed)
func move_controller(event: InputEventMouseMotion, controller: XRController3D):
var movement = Vector3()
movement += camera.global_transform.basis.x * event.relative.x * device_x_sensitivity/1000
movement += camera.global_transform.basis.y * event.relative.y * -device_y_sensitivity/1000
controller.global_translate(movement)
func attract_controller(event: InputEventMouseButton, controller: XRController3D):
var direction = -1
if not event.pressed:
return
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
direction = 1
elif event.button_index != MOUSE_BUTTON_WHEEL_DOWN:
return
var distance_vector = controller.global_transform.origin - camera.global_transform.origin
var forward = distance_vector.normalized() * direction
var movement = distance_vector + forward * (scroll_sensitivity/20)
if distance_vector.length() > 0.1 and movement.length() > 0.1:
controller.global_translate(forward * (scroll_sensitivity/20))
func rotate_device(event: InputEventMouseMotion, device: Node3D):
var motion = event.relative
device.rotate_y(motion.x * -device_x_sensitivity/1000)
device.rotate(device.transform.basis.x, motion.y * -device_y_sensitivity/1000)
func vector_key_mapping(key_positive_x: int, key_negative_x: int, key_positive_y: int, key_negative_y: int):
var x = 0
var y = 0
if Input.is_physical_key_pressed (key_positive_y):
y = 1
elif Input.is_physical_key_pressed (key_negative_y):
y = -1
if Input.is_physical_key_pressed (key_positive_x):
x = 1
elif Input.is_physical_key_pressed (key_negative_x):
x = -1
var vec = Vector2(x, y)
if vec:
vec = vec.normalized()
return vec

View File

@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=3 uid="uid://ctltchlf2j2r4"]
[ext_resource type="Script" path="res://addons/xr-simulator/XRSimulator.gd" id="1_uljju"]
[node name="XRSimulator" type="Node"]
script = ExtResource("1_uljju")
enabled = true

3
assets/banner.png Normal file
View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f70162100261f12060f1c82a27d6bc547a465c4f401c7914b3b2d0ed51d78f81
size 431818

3
assets/banner.png.import Normal file
View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1d859d4114970ee061de56a0c9e108deddaac675613b45ea1c6ac09a6225d63e
size 759

3
assets/design.afdesign Normal file
View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c48298302f3b19b61742398aa55b7e554074f72632cee50db3cde6696d60cdaa
size 3305459

3
assets/logo.png Normal file
View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b5d79d4a0c78bcce53fb01fa0353ec971eca5f6f8f3fb1fa7322f18f87a4781b
size 1122652

3
assets/logo.png.import Normal file
View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1daf27643292c3a222f2a9e60dfe38db5e62963cbfa2dd313588aa98f4e053b4
size 753

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:55d5f30db336a8f8f7633743c23e41077d5c1a1b900c475a6d3811746eba3d1b
size 141

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:25ef12c9534c8bbb08a816492562253c5c6fe0257f87427ce77b223231b07de5
size 299

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eead3733b7242e93c1739fcebd649efd1ce56ad1a3934e69d850c6ef3df66b9c
size 351

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:06c7745de9d5236d8096979e16708530a3c61112b604a7603033ea8bc9097791
size 258636

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b936cb0718d6b36ba9070d10fe2a16b2dbff0a4f9649e80205e3218fdb4b9df9
size 1007

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3fa30c33f89997925bc7164245d51209879a912597a72b227b861f9a9841acec
size 249715

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:71e68850377c2368a7433084c550cc7c5fa5110184cf59fc6d7a8f77f5f4af89
size 1017

View File

@ -1 +0,0 @@
<svg height="128" width="128" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="124" height="124" rx="14" fill="#363d52" stroke="#212532" stroke-width="4"/><g transform="scale(.101) translate(122 122)"><g fill="#fff"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 813 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H447l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c3 34 55 34 58 0v-86c-3-34-55-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 950 B

View File

@ -1,37 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bnc4gf261swvs"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

11
main.gd Normal file
View File

@ -0,0 +1,11 @@
extends Node3D
var sky = preload("res://assets/materials/sky.material")
var sky_passthrough = preload("res://assets/materials/sky_passthrough.material")
@onready var environment: WorldEnvironment = $WorldEnvironment
func _ready():
# In case we're running on the headset, use the passthrough sky
if OS.get_name() == "Android":
environment.environment.sky.set_material(sky_passthrough)

View File

@ -1,29 +1,78 @@
[gd_scene load_steps=4 format=3 uid="uid://18sldbn0hij8"] [gd_scene load_steps=12 format=3 uid="uid://eecv28y6jxk4"]
[ext_resource type="PackedScene" uid="uid://b4kad2kuba1yn" path="res://addons/godot-xr-tools/hands/scenes/lowpoly/left_hand_low.tscn" id="1_66jmx"] [ext_resource type="PackedScene" uid="uid://clc5dre31iskm" path="res://addons/godot-xr-tools/xr/start_xr.tscn" id="1_i4c04"]
[ext_resource type="PackedScene" uid="uid://l2n30mpbkdyw" path="res://addons/godot-xr-tools/hands/scenes/lowpoly/right_hand_low.tscn" id="2_3f5tl"] [ext_resource type="Script" path="res://src/raycast.gd" id="1_tsqxc"]
[ext_resource type="PackedScene" uid="uid://clc5dre31iskm" path="res://addons/godot-xr-tools/xr/start_xr.tscn" id="3_iaq1p"] [ext_resource type="Script" path="res://main.gd" id="1_uvrd4"]
[ext_resource type="Script" path="res://src/model.gd" id="2_7f1x4"]
[ext_resource type="PackedScene" uid="uid://c3kdssrmv84kv" path="res://scenes/menu.tscn" id="3_1tbp3"]
[ext_resource type="PackedScene" uid="uid://ctltchlf2j2r4" path="res://addons/xr-simulator/XRSimulator.tscn" id="5_3qc8g"]
[ext_resource type="Material" uid="uid://bf5ina366dwm6" path="res://assets/materials/sky.material" id="5_wgwf8"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_m58yb"]
ao_enabled = true
[sub_resource type="BoxMesh" id="BoxMesh_ir3co"]
material = SubResource("StandardMaterial3D_m58yb")
size = Vector3(0.01, 0.01, 0.01)
[sub_resource type="Sky" id="Sky_vhymk"]
sky_material = ExtResource("5_wgwf8")
[sub_resource type="Environment" id="Environment_7ghp0"]
background_mode = 2
background_color = Color(0.466667, 0.47451, 0.462745, 0)
sky = SubResource("Sky_vhymk")
ambient_light_color = Color(1, 1, 1, 1)
ambient_light_sky_contribution = 0.72
[node name="Main" type="Node3D"] [node name="Main" type="Node3D"]
transform = Transform3D(1, -0.000296142, 0.000270963, 0.000296143, 1, -4.61078e-06, -0.000270962, 4.67014e-06, 1, 0, 0, 0)
script = ExtResource("1_uvrd4")
[node name="XROrigin3D" type="XROrigin3D" parent="."] [node name="XROrigin3D" type="XROrigin3D" parent="."]
[node name="XRCamera3D" type="XRCamera3D" parent="XROrigin3D"] [node name="XRCamera3D" type="XRCamera3D" parent="XROrigin3D"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6555, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.798091, 0.311748)
[node name="LeftHand" type="XRController3D" parent="XROrigin3D"] [node name="XRControllerLeft" type="XRController3D" parent="XROrigin3D"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.341606, 0.506389, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.469893, 0.597213, -0.251112)
tracker = &"left_hand" tracker = &"left_hand"
pose = &"aim" pose = &"aim"
[node name="LeftHand" parent="XROrigin3D/LeftHand" instance=ExtResource("1_66jmx")] [node name="MeshInstance3D" type="MeshInstance3D" parent="XROrigin3D/XRControllerLeft"]
mesh = SubResource("BoxMesh_ir3co")
[node name="RightHand" type="XRController3D" parent="XROrigin3D"] [node name="Model" type="Node3D" parent="XROrigin3D/XRControllerLeft"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.408271, 0.538159, 0) script = ExtResource("2_7f1x4")
[node name="Menu" parent="XROrigin3D/XRControllerLeft" instance=ExtResource("3_1tbp3")]
transform = Transform3D(-4.37114e-08, 0, -1, -0.707107, 0.707107, 3.09086e-08, 0.707107, 0.707107, -3.09086e-08, 0.183517, 0, -0.0534939)
[node name="XRControllerRight" type="XRController3D" parent="XROrigin3D"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.488349, 0.559219, -0.2988)
tracker = &"right_hand" tracker = &"right_hand"
pose = &"aim" pose = &"aim"
[node name="RightHand" parent="XROrigin3D/RightHand" instance=ExtResource("2_3f5tl")] [node name="MeshInstance3D" type="MeshInstance3D" parent="XROrigin3D/XRControllerRight"]
mesh = SubResource("BoxMesh_ir3co")
[node name="StartXR" parent="." instance=ExtResource("3_iaq1p")] [node name="Raycast" type="Node3D" parent="XROrigin3D/XRControllerRight" node_paths=PackedStringArray("ray")]
script = ExtResource("1_tsqxc")
ray = NodePath("RayCast3D")
[node name="RayCast3D" type="RayCast3D" parent="XROrigin3D/XRControllerRight/Raycast"]
transform = Transform3D(1.91069e-15, 4.37114e-08, 1, 1, -4.37114e-08, 0, 4.37114e-08, 1, -4.37114e-08, 0, 0, 0)
target_position = Vector3(0, -5, 0)
[node name="StartXR" parent="." instance=ExtResource("1_i4c04")]
enable_passthrough = true enable_passthrough = true
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = SubResource("Environment_7ghp0")
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
transform = Transform3D(0.834925, -0.386727, -0.39159, 0.550364, 0.586681, 0.594058, 0, -0.711511, 0.702675, 0, 7.21041, 2.06458)
shadow_enabled = true
[node name="XRSimulator" parent="." instance=ExtResource("5_3qc8g")]
xr_origin = NodePath("../XROrigin3D")

File diff suppressed because it is too large Load Diff

View File

@ -11,13 +11,25 @@ config_version=5
[application] [application]
config/name="ImmersiveHome" config/name="ImmersiveHome"
run/main_scene="res://main.tscn"
config/features=PackedStringArray("4.1", "Mobile") config/features=PackedStringArray("4.1", "Mobile")
config/icon="res://icon.svg"
[autoload]
XRToolsUserSettings="*res://addons/godot-xr-tools/user_settings/user_settings.gd"
Request="*res://src/globals/request.gd"
HomeAdapters="*res://src/globals/home_adapters.gd"
[editor_plugins]
enabled=PackedStringArray("res://addons/godot-xr-tools/plugin.cfg")
[rendering] [rendering]
renderer/rendering_method="mobile" renderer/rendering_method="mobile"
textures/vram_compression/import_etc2_astc=true textures/vram_compression/import_etc2_astc=true
lights_and_shadows/directional_shadow/soft_shadow_filter_quality=4
lights_and_shadows/directional_shadow/soft_shadow_filter_quality.mobile=4
[xr] [xr]

23
scenes/device.tscn Normal file
View File

@ -0,0 +1,23 @@
[gd_scene load_steps=4 format=3 uid="uid://dbe8slnyhro2n"]
[ext_resource type="Script" path="res://src/ui/device.gd" id="1_rbo86"]
[sub_resource type="BoxMesh" id="BoxMesh_aa3i4"]
size = Vector3(0.05, 0.01, 0.05)
[sub_resource type="BoxShape3D" id="BoxShape3D_28fjq"]
size = Vector3(0.05, 0.01, 0.05)
[node name="Device" type="StaticBody3D"]
script = ExtResource("1_rbo86")
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
mesh = SubResource("BoxMesh_aa3i4")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
shape = SubResource("BoxShape3D_28fjq")
[node name="Label" type="Label3D" parent="."]
transform = Transform3D(-2.18557e-09, -0.05, -2.18557e-09, 0, -2.18557e-09, 0.05, -0.05, 2.18557e-09, 9.55343e-17, 0, 0.00918245, 0)
text = "Text"
autowrap_mode = 3

View File

@ -0,0 +1,33 @@
[gd_scene load_steps=6 format=3 uid="uid://cw86rc42dv2d8"]
[ext_resource type="Script" path="res://src/entities/light.gd" id="1_ykxy3"]
[ext_resource type="Texture2D" uid="uid://b72vsbcvqqxg7" path="res://assets/materials/swich_on.png" id="2_6gn2e"]
[ext_resource type="Texture2D" uid="uid://cvc0o6dsktnvl" path="res://assets/materials/switch_off.png" id="3_qlm62"]
[sub_resource type="SphereShape3D" id="SphereShape3D_ukj14"]
radius = 0.1
[sub_resource type="SpriteFrames" id="SpriteFrames_ldpuo"]
animations = [{
"frames": [{
"duration": 1.0,
"texture": ExtResource("2_6gn2e")
}, {
"duration": 1.0,
"texture": ExtResource("3_qlm62")
}],
"loop": true,
"name": &"default",
"speed": 5.0
}]
[node name="Light" type="StaticBody3D"]
script = ExtResource("1_ykxy3")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
shape = SubResource("SphereShape3D_ukj14")
[node name="Icon" type="AnimatedSprite3D" parent="."]
pixel_size = 0.0005
billboard = 1
sprite_frames = SubResource("SpriteFrames_ldpuo")

View File

@ -0,0 +1,15 @@
[gd_scene load_steps=3 format=3 uid="uid://xsiy71rsqulj"]
[ext_resource type="Script" path="res://src/entities/sensor.gd" id="1_57ac8"]
[sub_resource type="SphereShape3D" id="SphereShape3D_r20gc"]
radius = 0.1
[node name="Sensor" type="StaticBody3D"]
script = ExtResource("1_57ac8")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
shape = SubResource("SphereShape3D_r20gc")
[node name="Label" type="Label3D" parent="."]
text = "some text"

View File

@ -0,0 +1,33 @@
[gd_scene load_steps=6 format=3 uid="uid://cscl5k7lhopj5"]
[ext_resource type="Script" path="res://src/entities/switch.gd" id="1_8ffhi"]
[ext_resource type="Texture2D" uid="uid://b72vsbcvqqxg7" path="res://assets/materials/swich_on.png" id="1_w68gw"]
[ext_resource type="Texture2D" uid="uid://cvc0o6dsktnvl" path="res://assets/materials/switch_off.png" id="2_86ba1"]
[sub_resource type="SphereShape3D" id="SphereShape3D_ukj14"]
radius = 0.1
[sub_resource type="SpriteFrames" id="SpriteFrames_ldpuo"]
animations = [{
"frames": [{
"duration": 1.0,
"texture": ExtResource("1_w68gw")
}, {
"duration": 1.0,
"texture": ExtResource("2_86ba1")
}],
"loop": true,
"name": &"default",
"speed": 5.0
}]
[node name="Switch" type="StaticBody3D"]
script = ExtResource("1_8ffhi")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
shape = SubResource("SphereShape3D_ukj14")
[node name="Icon" type="AnimatedSprite3D" parent="."]
pixel_size = 0.0005
billboard = 1
sprite_frames = SubResource("SpriteFrames_ldpuo")

23
scenes/entity.tscn Normal file
View File

@ -0,0 +1,23 @@
[gd_scene load_steps=4 format=3 uid="uid://xo0o5nrfjl23"]
[ext_resource type="Script" path="res://src/ui/entity.gd" id="1_825oj"]
[sub_resource type="BoxMesh" id="BoxMesh_aa3i4"]
size = Vector3(0.05, 0.01, 0.05)
[sub_resource type="BoxShape3D" id="BoxShape3D_28fjq"]
size = Vector3(0.05, 0.01, 0.05)
[node name="Entity" type="StaticBody3D"]
script = ExtResource("1_825oj")
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
mesh = SubResource("BoxMesh_aa3i4")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
shape = SubResource("BoxShape3D_28fjq")
[node name="Label" type="Label3D" parent="."]
transform = Transform3D(-2.18557e-09, -0.05, -2.18557e-09, 0, -2.18557e-09, 0.05, -0.05, 2.18557e-09, 9.55343e-17, 0, 0.00918245, 0)
text = "Text"
autowrap_mode = 3

17
scenes/menu.tscn Normal file
View File

@ -0,0 +1,17 @@
[gd_scene load_steps=4 format=3 uid="uid://c3kdssrmv84kv"]
[ext_resource type="Script" path="res://src/menu.gd" id="1_ng4u3"]
[ext_resource type="Material" uid="uid://bertj8bp8b5l1" path="res://assets/materials/interface.tres" id="2_nsukb"]
[sub_resource type="PlaneMesh" id="PlaneMesh_6t3dn"]
material = ExtResource("2_nsukb")
size = Vector2(0.3, 0.3)
[node name="Menu" type="Node3D"]
script = ExtResource("1_ng4u3")
[node name="Background" type="MeshInstance3D" parent="."]
mesh = SubResource("PlaneMesh_6t3dn")
[node name="Devices" type="Node3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.149223, 0, 0.150667)

23
src/entities/light.gd Normal file
View File

@ -0,0 +1,23 @@
extends StaticBody3D
@export var entity_id = "switch.plug_printer_2"
@onready var sprite: AnimatedSprite3D = $Icon
# Called when the node enters the scene tree for the first time.
func _ready():
var stateInfo = await HomeAdapters.adapter_ws.get_state(entity_id)
if stateInfo["state"] == "on":
sprite.set_frame(0)
else:
sprite.set_frame(1)
func _on_toggle():
HomeAdapters.adapter_ws.set_state(entity_id, "off" if sprite.get_frame() == 0 else "on")
if sprite.get_frame() == 0:
sprite.set_frame(1)
else:
sprite.set_frame(0)
func _on_request_completed():
pass

16
src/entities/sensor.gd Normal file
View File

@ -0,0 +1,16 @@
extends StaticBody3D
@export var entity_id = "sensor.sun_next_dawn"
@onready var label: Label3D = $Label
# Called when the node enters the scene tree for the first time.
func _ready():
var stateInfo = await HomeAdapters.adapter_ws.get_state(entity_id)
label.text = stateInfo["state"]
await HomeAdapters.adapter_ws.watch_state(entity_id, func(new_state):
label.text = new_state["state"]
)
func _on_toggle():
pass

30
src/entities/switch.gd Normal file
View File

@ -0,0 +1,30 @@
extends StaticBody3D
@export var entity_id = "switch.plug_printer_2"
@onready var sprite: AnimatedSprite3D = $Icon
# Called when the node enters the scene tree for the first time.
func _ready():
var stateInfo = await HomeAdapters.adapter_ws.get_state(entity_id)
if stateInfo["state"] == "on":
sprite.set_frame(0)
else:
sprite.set_frame(1)
await HomeAdapters.adapter_ws.watch_state(entity_id, func(new_state):
if new_state["state"] == "on":
sprite.set_frame(0)
else:
sprite.set_frame(1)
)
func _on_toggle():
HomeAdapters.adapter_ws.set_state(entity_id, "off" if sprite.get_frame() == 0 else "on")
if sprite.get_frame() == 0:
sprite.set_frame(1)
else:
sprite.set_frame(0)
func _on_request_completed():
pass

View File

@ -0,0 +1,11 @@
extends Node
var Adapter = preload("res://src/home_adapters/adapter.gd")
var adapter = Adapter.new(Adapter.ADAPTER_TYPES.HASS)
var adapter_ws = Adapter.new(Adapter.ADAPTER_TYPES.HASS_WS)
func _ready():
add_child(adapter)
add_child(adapter_ws)

1
src/globals/request.gd Normal file
View File

@ -0,0 +1 @@
extends HTTPRequest

View File

@ -0,0 +1,41 @@
extends Node
const hass = preload("res://src/home_adapters/hass/hass.gd")
const hass_ws = preload("res://src/home_adapters/hass_ws/hass.gd")
enum ADAPTER_TYPES {
HASS,
HASS_WS
}
const adapters = {
ADAPTER_TYPES.HASS: hass,
ADAPTER_TYPES.HASS_WS: hass_ws
}
const methods = [
"load_devices",
"get_state",
"set_state"
]
var adapter: Node
func _init(type: ADAPTER_TYPES):
adapter = adapters[type].new()
add_child(adapter)
for method in methods:
assert(adapter.has_method(method), "Adapter does not implement method: " + method)
func load_devices():
return await adapter.load_devices()
func get_state(entity: String):
return await adapter.get_state(entity)
func set_state(entity: String, state: String, attributes: Dictionary = {}):
return await adapter.set_state(entity, state, attributes)
func watch_state(entity: String, callback: Callable):
return adapter.watch_state(entity, callback)

View File

@ -0,0 +1,60 @@
extends Node
var url: String = "http://192.168.33.33:8123"
var token: String = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzZjQ0ZGM2N2Y3YzY0MDc1OGZlMWI2ZjJlNmIxZjRkNSIsImlhdCI6MTY5ODAxMDcyOCwiZXhwIjoyMDEzMzcwNzI4fQ.K6ydLUC-4Q7BNIRCU1nWlI2s6sg9UCiOu-Lpedw2zJc"
var headers: PackedStringArray = PackedStringArray([])
var devices_template = FileAccess.get_file_as_string("res://src/home_adapters/hass/templates/devices.j2")
func _init(url := self.url, token := self.token):
self.url = url
self.token = token
headers = PackedStringArray(["Authorization: Bearer %s" % token, "Content-Type: application/json"])
devices_template = devices_template.replace("\n", " ").replace("\t", "").replace("\r", " ").replace("\"", "\\\"")
func load_devices():
Request.request("%s/api/template" % [url], headers, HTTPClient.METHOD_POST, "{\"template\": \"%s\"}" % [devices_template])
var response = await Request.request_completed
var data_string = response[3].get_string_from_utf8().replace("'", "\"")
var json = JSON.parse_string(data_string).data
return json
func get_state(entity: String):
var type = entity.split('.')[0]
Request.request("%s/api/states/%s" % [url, entity], headers, HTTPClient.METHOD_GET)
var response = await Request.request_completed
var data_string = response[3].get_string_from_utf8().replace("'", "\"")
var json = JSON.parse_string(data_string)
return json
func set_state(entity: String, state: String, attributes: Dictionary = {}):
var type = entity.split('.')[0]
var response
if type == 'switch':
if state == 'on':
Request.request("%s/api/services/switch/turn_on" % [url], headers, HTTPClient.METHOD_POST, "{\"entity_id\": \"%s\"}" % [entity])
response = await Request.request_completed
elif state == 'off':
Request.request("%s/api/services/switch/turn_off" % [url], headers, HTTPClient.METHOD_POST, "{\"entity_id\": \"%s\"}" % [entity])
response = await Request.request_completed
elif type == 'light':
if state == 'on':
Request.request("%s/api/services/light/turn_on" % [url], headers, HTTPClient.METHOD_POST, "{\"entity_id\": \"%s\"}" % [entity])
response = await Request.request_completed
elif state == 'off':
Request.request("%s/api/services/light/turn_off" % [url], headers, HTTPClient.METHOD_POST, "{\"entity_id\": \"%s\"}" % [entity])
response = await Request.request_completed
var data_string = response[3].get_string_from_utf8().replace("'", "\"")
var json = JSON.parse_string(data_string)
return json

View File

@ -0,0 +1,12 @@
{% set devices = states | map(attribute='entity_id') | map('device_id') | unique | reject('eq',None) | list %}
{%- set ns = namespace(devices = []) %}
{%- for device in devices %}
{%- set entities = device_entities(device) | list %}
{%- if entities %}
{%- set ns.devices = ns.devices + [ {device: {"name": device_attr(device, "name"), "entities": entities }} ] %}
{%- endif %}
{%- endfor %}
{
"data": {{ ns.devices }}
}

View File

@ -0,0 +1,40 @@
extends Node
class_name CallbackMap
var callbacks := {}
func add(key: Variant, callback: Callable) -> void:
_validate_key(key)
if callbacks.has(key):
callbacks[key].append(callback)
else:
callbacks[key] = [callback]
func add_once(key: Variant, callback: Callable) -> void:
_validate_key(key)
var fn: Callable
fn = func(args: Array):
remove(key, fn)
callback.callv(args)
add(key, fn)
func remove(key: Variant, callback: Callable) -> void:
_validate_key(key)
if callbacks.has(key):
callbacks[key].erase(callback)
func call_key(key: Variant, args: Array) -> void:
_validate_key(key)
if callbacks.has(key):
for callback in callbacks[key]:
callback.callv(args)
func _validate_key(key: Variant):
assert(typeof(key) == TYPE_STRING || typeof(key) == TYPE_INT || typeof(key) == TYPE_FLOAT, "key must be a string or number")

View File

@ -0,0 +1,222 @@
extends Node
var devices_template := FileAccess.get_file_as_string("res://src/home_adapters/hass/templates/devices.j2")
var socket := WebSocketPeer.new()
# in seconds
var request_timeout := 10.0
var url := "ws://192.168.33.33:8123/api/websocket"
var token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzZjQ0ZGM2N2Y3YzY0MDc1OGZlMWI2ZjJlNmIxZjRkNSIsImlhdCI6MTY5ODAxMDcyOCwiZXhwIjoyMDEzMzcwNzI4fQ.K6ydLUC-4Q7BNIRCU1nWlI2s6sg9UCiOu-Lpedw2zJc"
var LOG_MESSAGES := false
var authenticated := false
var id := 1
var entities: Dictionary = {}
var entitiy_callbacks := CallbackMap.new()
var packet_callbacks := CallbackMap.new()
signal on_connect()
signal on_disconnect()
func _init(url := self.url, token := self.token):
self.url = url
self.token = token
devices_template = devices_template.replace("\n", " ").replace("\t", "").replace("\r", " ").replace("\"", "\\\"")
connect_ws()
func connect_ws():
print("Connecting to %s" % self.url)
socket.connect_to_url(self.url)
# https://github.com/godotengine/godot/issues/84423
# Otherwise the WebSocketPeer will crash when receiving large packets
socket.set_inbound_buffer_size(65535 * 2)
func _process(delta):
socket.poll()
var state = socket.get_ready_state()
if state == WebSocketPeer.STATE_OPEN:
while socket.get_available_packet_count():
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)
elif state == WebSocketPeer.STATE_CLOSING:
pass
elif state == WebSocketPeer.STATE_CLOSED:
var code = socket.get_close_code()
var reason = socket.get_close_reason()
print("WS connection closed with code: %s, reason: %s" % [code, reason])
handle_disconnect()
func handle_packet(packet: Dictionary):
if LOG_MESSAGES: print("Received packet: %s" % packet)
if packet.type == "auth_required":
send_packet({
"type": "auth",
"access_token": self.token
})
elif packet.type == "auth_ok":
authenticated = true
start_subscriptions()
elif packet.type == "auth_invalid":
handle_disconnect()
else:
packet_callbacks.call_key(int(packet.id), [packet])
func start_subscriptions():
assert(authenticated, "Not authenticated")
# await send_request_packet({
# "type": "supported_features",
# "features": {
# "coalesce_messages": 1
# }
# })
# await send_request_packet({
# "type": "subscribe_events",
# "event_type": "state_changed"
# })
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():
entities[entity] = {
"state": packet.event.a[entity]["s"],
"attributes": packet.event.a[entity]["a"]
}
entitiy_callbacks.call_key(entity, [entities[entity]])
on_connect.emit()
if packet.event.has("c"):
for entity in packet.event.c.keys():
if !entities.has(entity):
continue
if packet.event.c[entity].has("+"):
if packet.event.c[entity]["+"].has("s"):
entities[entity]["state"] = packet.event.c[entity]["+"]["s"]
if packet.event.c[entity]["+"].has("a"):
entities[entity]["attributes"].merge(packet.event.c[entity]["+"]["a"])
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)
send_packet(packet)
return func():
packet_callbacks.remove(packet.id, callback)
send_packet({
id: id,
"type": packet.type.replace("subscribe", "unsubscribe"),
"subscription": packet.id
})
id += 1
func send_request_packet(packet: Dictionary):
packet.id = id
id += 1
send_packet(packet)
var promise = Promise.new(func(resolve: Callable, reject: Callable):
packet_callbacks.add_once(packet.id, resolve)
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"))
packet_callbacks.remove(packet.id, resolve)
)
add_child(timeout)
timeout.start()
)
return await promise.settled
func send_packet(packet: Dictionary):
if LOG_MESSAGES || true: print("Sending packet: %s" % encode_packet(packet))
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)
func load_devices():
if !authenticated:
await on_connect
return entities
func get_state(entity: String):
if !authenticated:
await on_connect
if entities.has(entity):
return entities[entity]
return null
func watch_state(entity: String, callback: Callable):
if !authenticated:
await on_connect
entitiy_callbacks.add(entity, callback)
func set_state(entity: String, state: String, attributes: Dictionary = {}):
assert(authenticated, "Not authenticated")
var domain = entity.split(".")[0]
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'
return await send_request_packet({
"type": "call_service",
"domain": domain,
"service": service,
"service_data": attributes,
"target": {
"entity_id": entity
}
})

View File

@ -0,0 +1,12 @@
{% set devices = states | map(attribute='entity_id') | map('device_id') | unique | reject('eq',None) | list %}
{%- set ns = namespace(devices = []) %}
{%- for device in devices %}
{%- set entities = device_entities(device) | list %}
{%- if entities %}
{%- set ns.devices = ns.devices + [ {device: {"name": device_attr(device, "name"), "entities": entities }} ] %}
{%- endif %}
{%- endfor %}
{
"data": {{ ns.devices }}
}

111
src/menu.gd Normal file
View File

@ -0,0 +1,111 @@
extends Node3D
const Device = preload("res://scenes/device.tscn")
const Entity = preload("res://scenes/entity.tscn")
const Switch = preload("res://scenes/entities/switch.tscn")
const Light = preload("res://scenes/entities/light.tscn")
const Sensor = preload("res://scenes/entities/sensor.tscn")
@onready var devices_node = $Devices
var devices
var selected_device = null
# Called when the node enters the scene tree for the first time.
func _ready():
devices = await HomeAdapters.adapter.load_devices()
render_devices()
func render_devices():
var x = 0
var y = 0
for device in devices:
var info = device.values()[0]
var device_instance = Device.instantiate()
device_instance.set_position(Vector3(y * 0.08, 0, -x * 0.08))
device_instance.click.connect(_on_device_click)
device_instance.id = device.keys()[0]
devices_node.add_child(device_instance)
device_instance.set_device_name(info["name"])
x += 1
if x % 5 == 0:
x = 0
y += 1
func render_entities():
var x = 0
var y = 0
var info
for device in devices:
if device.keys()[0] == selected_device:
info = device.values()[0]
break
if info == null:
return
var entities = info["entities"]
for entity in entities:
var entity_instance = Entity.instantiate()
entity_instance.set_position(Vector3(y * 0.08, 0, -x * 0.08))
entity_instance.click.connect(_on_entity_click)
devices_node.add_child(entity_instance)
entity_instance.set_entity_name(entity)
x += 1
if x % 5 == 0:
x = 0
y += 1
func _on_device_click(device_id):
selected_device = device_id
print(selected_device)
clear_menu()
render_entities()
func _on_entity_click(entity_name):
print(entity_name)
selected_device = null
clear_menu()
render_devices()
var type = entity_name.split(".")[0]
print(type)
if type == "switch":
var switch = Switch.instantiate()
switch.entity_id = entity_name
switch.set_position(global_position)
get_node("/root").add_child(switch)
if type == "light":
var light = Light.instantiate()
light.entity_id = entity_name
light.set_position(global_position)
get_node("/root").add_child(light)
if type == "sensor":
var sensor = Sensor.instantiate()
sensor.entity_id = entity_name
sensor.set_position(global_position)
get_node("/root").add_child(sensor)
func clear_menu():
for child in devices_node.get_children():
devices_node.remove_child(child)
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
pass

23
src/model.gd Normal file
View File

@ -0,0 +1,23 @@
extends Node3D
@onready var _controller := XRHelpers.get_xr_controller(self)
@export var light: StaticBody3D
# Called when the node enters the scene tree for the first time.
func _ready():
_controller.button_pressed.connect(self._on_button_pressed)
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
pass
func _on_button_pressed(button):
print("right: ", button)
if button != "trigger_click":
return
if light == null:
return
# set light position to controller position
light.transform.origin = _controller.transform.origin

27
src/raycast.gd Normal file
View File

@ -0,0 +1,27 @@
extends Node3D
@onready var _controller := XRHelpers.get_xr_controller(self)
@export var ray: RayCast3D
# Called when the node enters the scene tree for the first time.
func _ready():
_controller.button_pressed.connect(self._on_button_pressed)
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
pass
func _on_button_pressed(button):
print(button)
if button != "trigger_click":
return
var collider = ray.get_collider()
if collider == null:
return
print(collider)
if collider.has_method("_on_toggle"):
collider._on_toggle()

13
src/ui/device.gd Normal file
View File

@ -0,0 +1,13 @@
extends StaticBody3D
@onready var label: Label3D = $Label
@export var id: String = "0"
signal click(id: String)
func _on_toggle():
click.emit(id)
func set_device_name(text):
assert(label != null, "Device has to be added to the scene tree")
label.text = text

14
src/ui/entity.gd Normal file
View File

@ -0,0 +1,14 @@
extends StaticBody3D
@onready var label: Label3D = $Label
@export var text = "Default"
signal click(name: String)
func _on_toggle():
click.emit(text)
func set_entity_name(text):
assert(label != null, "Entity has to be added to the scene tree")
label.text = text.replace(".", "\n")
self.text = text