Merge pull request #11 from Nitwel/testing
Update main to latest progress
This commit is contained in:
commit
9e2f46784d
12
.editorconfig
Normal file
12
.editorconfig
Normal 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
2
.gitattributes
vendored
|
@ -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
|
||||||
|
|
95
README.md
95
README.md
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
21
addons/promise/LICENSE
Normal 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
165
addons/promise/promise.gd
Normal 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
|
242
addons/xr-simulator/XRSimulator.gd
Normal file
242
addons/xr-simulator/XRSimulator.gd
Normal 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
|
7
addons/xr-simulator/XRSimulator.tscn
Normal file
7
addons/xr-simulator/XRSimulator.tscn
Normal 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
3
assets/banner.png
Normal 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
3
assets/banner.png.import
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1d859d4114970ee061de56a0c9e108deddaac675613b45ea1c6ac09a6225d63e
|
||||||
|
size 759
|
3
assets/design.afdesign
Normal file
3
assets/design.afdesign
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c48298302f3b19b61742398aa55b7e554074f72632cee50db3cde6696d60cdaa
|
||||||
|
size 3305459
|
3
assets/logo.png
Normal file
3
assets/logo.png
Normal 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
3
assets/logo.png.import
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1daf27643292c3a222f2a9e60dfe38db5e62963cbfa2dd313588aa98f4e053b4
|
||||||
|
size 753
|
3
assets/materials/interface.tres
Normal file
3
assets/materials/interface.tres
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:55d5f30db336a8f8f7633743c23e41077d5c1a1b900c475a6d3811746eba3d1b
|
||||||
|
size 141
|
3
assets/materials/sky.material
Normal file
3
assets/materials/sky.material
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:25ef12c9534c8bbb08a816492562253c5c6fe0257f87427ce77b223231b07de5
|
||||||
|
size 299
|
3
assets/materials/sky_passthrough.material
Normal file
3
assets/materials/sky_passthrough.material
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:eead3733b7242e93c1739fcebd649efd1ce56ad1a3934e69d850c6ef3df66b9c
|
||||||
|
size 351
|
3
assets/materials/swich_on.png
Normal file
3
assets/materials/swich_on.png
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:06c7745de9d5236d8096979e16708530a3c61112b604a7603033ea8bc9097791
|
||||||
|
size 258636
|
3
assets/materials/swich_on.png.import
Normal file
3
assets/materials/swich_on.png.import
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b936cb0718d6b36ba9070d10fe2a16b2dbff0a4f9649e80205e3218fdb4b9df9
|
||||||
|
size 1007
|
3
assets/materials/switch_off.png
Normal file
3
assets/materials/switch_off.png
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3fa30c33f89997925bc7164245d51209879a912597a72b227b861f9a9841acec
|
||||||
|
size 249715
|
3
assets/materials/switch_off.png.import
Normal file
3
assets/materials/switch_off.png.import
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:71e68850377c2368a7433084c550cc7c5fa5110184cf59fc6d7a8f77f5f4af89
|
||||||
|
size 1017
|
1
icon.svg
1
icon.svg
|
@ -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 |
|
@ -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
11
main.gd
Normal 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)
|
73
main.tscn
73
main.tscn
|
@ -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
|
@ -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
23
scenes/device.tscn
Normal 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
|
33
scenes/entities/light.tscn
Normal file
33
scenes/entities/light.tscn
Normal 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")
|
15
scenes/entities/sensor.tscn
Normal file
15
scenes/entities/sensor.tscn
Normal 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"
|
33
scenes/entities/switch.tscn
Normal file
33
scenes/entities/switch.tscn
Normal 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
23
scenes/entity.tscn
Normal 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
17
scenes/menu.tscn
Normal 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
23
src/entities/light.gd
Normal 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
16
src/entities/sensor.gd
Normal 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
30
src/entities/switch.gd
Normal 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
|
11
src/globals/home_adapters.gd
Normal file
11
src/globals/home_adapters.gd
Normal 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
1
src/globals/request.gd
Normal file
|
@ -0,0 +1 @@
|
||||||
|
extends HTTPRequest
|
41
src/home_adapters/adapter.gd
Normal file
41
src/home_adapters/adapter.gd
Normal 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)
|
60
src/home_adapters/hass/hass.gd
Normal file
60
src/home_adapters/hass/hass.gd
Normal 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
|
||||||
|
|
12
src/home_adapters/hass/templates/devices.j2
Normal file
12
src/home_adapters/hass/templates/devices.j2
Normal 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 }}
|
||||||
|
}
|
40
src/home_adapters/hass_ws/callback_map.gd
Normal file
40
src/home_adapters/hass_ws/callback_map.gd
Normal 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")
|
222
src/home_adapters/hass_ws/hass.gd
Normal file
222
src/home_adapters/hass_ws/hass.gd
Normal 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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
12
src/home_adapters/hass_ws/templates/devices.j2
Normal file
12
src/home_adapters/hass_ws/templates/devices.j2
Normal 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
111
src/menu.gd
Normal 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
23
src/model.gd
Normal 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
27
src/raycast.gd
Normal 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
13
src/ui/device.gd
Normal 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
14
src/ui/entity.gd
Normal 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
|
Loading…
Reference in New Issue
Block a user