13.04.2026
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -26,6 +26,7 @@ share/python-wheels/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
node_modules/
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
|
||||
1
Mieszkanie Kuby/flows.json
Normal file
1
Mieszkanie Kuby/flows.json
Normal file
File diff suppressed because one or more lines are too long
97
Mieszkanie Kuby/main-hub.yaml
Normal file
97
Mieszkanie Kuby/main-hub.yaml
Normal file
@@ -0,0 +1,97 @@
|
||||
esphome:
|
||||
name: main-hub
|
||||
friendly_name: main-hub
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
logger:
|
||||
|
||||
api:
|
||||
encryption:
|
||||
key: "eyBNZWDUaBDCTIZsSIUzwHFpUcm/Kbc9UAWjX++ToL4="
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: "000f95472f1adc514d45abdc280a3903"
|
||||
|
||||
#wifi:
|
||||
# ssid: !secret wifi_ssid
|
||||
# password: !secret wifi_password
|
||||
#
|
||||
# ap:
|
||||
# ssid: "Main-Hub Fallback Hotspot"
|
||||
# password: "y0LGSOyT5tjJ"
|
||||
|
||||
#captive_portal:
|
||||
|
||||
web_server:
|
||||
port: 80
|
||||
|
||||
ethernet:
|
||||
type: LAN8720
|
||||
mdc_pin: GPIO23
|
||||
mdio_pin: GPIO18
|
||||
clk_mode: GPIO17_OUT
|
||||
phy_addr: 0
|
||||
power_pin: GPIO12
|
||||
|
||||
button:
|
||||
- platform: restart
|
||||
name: "main-hub Restart"
|
||||
|
||||
packages:
|
||||
i2c: !include packages/i2c.yaml
|
||||
pca9685: !include packages/pca9685.yaml
|
||||
mcp23017_outputs: !include packages/mcp23017_outputs.yaml
|
||||
mcp23017_inputs: !include packages/mcp23017_inputs.yaml
|
||||
scripts: !include packages/scripts.yaml
|
||||
|
||||
esp32_ble_tracker:
|
||||
|
||||
sensor:
|
||||
- platform: uptime
|
||||
type: seconds
|
||||
name: Uptime Sensor
|
||||
- platform: atc_mithermometer
|
||||
mac_address: "A4:C1:38:C5:11:58"
|
||||
temperature:
|
||||
name: "Biuro Temperatura"
|
||||
humidity:
|
||||
name: "Biuro Nawilżenie"
|
||||
battery_level:
|
||||
name: "Biuro Bateria"
|
||||
- platform: atc_mithermometer
|
||||
mac_address: "A4:C1:38:5C:4F:38"
|
||||
temperature:
|
||||
name: "Serwer Temperatura"
|
||||
humidity:
|
||||
name: "Serwer Nawilżenie"
|
||||
battery_level:
|
||||
name: "Serwer Bateria"
|
||||
- platform: atc_mithermometer
|
||||
mac_address: "A4:C1:38:67:6B:94"
|
||||
temperature:
|
||||
name: "Kuchnia Temperatura"
|
||||
humidity:
|
||||
name: "Kuchnia Nawilżenie"
|
||||
battery_level:
|
||||
name: "Kuchnia Bateria"
|
||||
- platform: atc_mithermometer
|
||||
mac_address: "A4:C1:38:C9:FE:CA"
|
||||
temperature:
|
||||
name: "WC Temperatura"
|
||||
humidity:
|
||||
name: "WC Nawilżenie"
|
||||
battery_level:
|
||||
name: "WC Bateria"
|
||||
- platform: atc_mithermometer
|
||||
mac_address: "A4:C1:38:63:6D:DB"
|
||||
temperature:
|
||||
name: "Łazienka Temperatura"
|
||||
humidity:
|
||||
name: "Łazienka Nawilżenie"
|
||||
battery_level:
|
||||
name: "Łazienka Bateria"
|
||||
5
Mieszkanie Kuby/packages/i2c.yaml
Normal file
5
Mieszkanie Kuby/packages/i2c.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
i2c:
|
||||
sda: 13
|
||||
scl: 16
|
||||
scan: True
|
||||
frequency: 200kHz
|
||||
884
Mieszkanie Kuby/packages/mcp23017_inputs.yaml
Normal file
884
Mieszkanie Kuby/packages/mcp23017_inputs.yaml
Normal file
@@ -0,0 +1,884 @@
|
||||
mcp23017:
|
||||
- id: "mcp23xxx_hub1_IN"
|
||||
address: 0x20
|
||||
- id: "mcp23xxx_hub2_IN"
|
||||
address: 0x22
|
||||
- id: "mcp23xxx_hub3_IN"
|
||||
address: 0x24
|
||||
|
||||
|
||||
binary_sensor:
|
||||
##################
|
||||
#mcp23xxx_hub1_IN#
|
||||
##################
|
||||
|
||||
# Wejście 1 — single: halogeny wejście, triple: wyłącz lampy, long: wyłącz wentylatory
|
||||
- platform: gpio
|
||||
name: "hub1_in0"
|
||||
id: hub1_in0
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub1_IN
|
||||
number: 0
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub1_out15
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- script.execute: off_all_lights
|
||||
- timing:
|
||||
- ON for at least 350ms
|
||||
then:
|
||||
- script.execute: off_all_fans
|
||||
|
||||
# Wejście 3 — brak automatyzacji w Node-RED
|
||||
- platform: gpio
|
||||
name: "hub1_in1"
|
||||
id: hub1_in1
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub1_IN
|
||||
number: 1
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
|
||||
# Wejście 2 — single: hub1_out14, triple: WLED korytarz kinkiety
|
||||
- platform: gpio
|
||||
name: "hub1_in2"
|
||||
id: hub1_in2
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub1_IN
|
||||
number: 2
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub1_out14
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- script.execute: wled_korytarz_kinkiety_toggle
|
||||
|
||||
# Sypialnia 3 — single: turn_on hub2_out12+hub2_out1, double: toggle hub2_out12+hub2_out1
|
||||
- platform: gpio
|
||||
name: "hub1_in3"
|
||||
id: hub1_in3
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub1_IN
|
||||
number: 3
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.turn_on: hub2_out12
|
||||
- light.turn_on: hub2_out1
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub2_out12
|
||||
- light.toggle: hub2_out1
|
||||
|
||||
# Sypialnia 1 — single: sypialnia LED
|
||||
- platform: gpio
|
||||
name: "hub1_in4"
|
||||
id: hub1_in4
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub1_IN
|
||||
number: 4
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: sypialnia_led
|
||||
|
||||
# Sypialnia 2 — single: hub2_out7
|
||||
- platform: gpio
|
||||
name: "hub1_in5"
|
||||
id: hub1_in5
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub1_IN
|
||||
number: 5
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub2_out7
|
||||
|
||||
# Sypialnia Łóżko Prawa Strona — single: hub2_out1, double: hub2_out12+hub2_out1, long: wyłącz wszystkie
|
||||
- platform: gpio
|
||||
name: "hub1_in6"
|
||||
id: hub1_in6
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub1_IN
|
||||
number: 6
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub2_out1
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub2_out12
|
||||
- light.toggle: hub2_out1
|
||||
- timing:
|
||||
- ON for at least 350ms
|
||||
then:
|
||||
- script.execute: off_all_lights
|
||||
- script.execute: off_all_fans
|
||||
|
||||
# Sypialnia Łóżko Lewa Strona — single: hub2_out12, double: hub2_out12+hub2_out1, long: wyłącz wszystkie
|
||||
- platform: gpio
|
||||
name: "hub1_in7"
|
||||
id: hub1_in7
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub1_IN
|
||||
number: 7
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub2_out12
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub2_out12
|
||||
- light.toggle: hub2_out1
|
||||
- timing:
|
||||
- ON for at least 350ms
|
||||
then:
|
||||
- script.execute: off_all_lights
|
||||
- script.execute: off_all_fans
|
||||
|
||||
# Niebieski Pokój Lewy — single: hub2_out10, double: hub1_out7
|
||||
- platform: gpio
|
||||
name: "hub1_in8"
|
||||
id: hub1_in8
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub1_IN
|
||||
number: 8
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub2_out10
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub1_out7
|
||||
|
||||
# Niebieski Pokój Prawy — bezpośredni toggle pokój dla gości LED (bez gestów)
|
||||
- platform: gpio
|
||||
name: "hub1_in9"
|
||||
id: hub1_in9
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub1_IN
|
||||
number: 9
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_click:
|
||||
min_length: 50ms
|
||||
max_length: 1000ms
|
||||
then:
|
||||
- light.toggle: pokoj_dla_gosci_led
|
||||
|
||||
# Nie podpięte
|
||||
- platform: gpio
|
||||
name: "hub1_in10"
|
||||
id: hub1_in10
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub1_IN
|
||||
number: 10
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
- platform: gpio
|
||||
name: "hub1_in11"
|
||||
id: hub1_in11
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub1_IN
|
||||
number: 11
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
- platform: gpio
|
||||
name: "hub1_in12"
|
||||
id: hub1_in12
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub1_IN
|
||||
number: 12
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
- platform: gpio
|
||||
name: "hub1_in13"
|
||||
id: hub1_in13
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub1_IN
|
||||
number: 13
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
- platform: gpio
|
||||
name: "hub1_in14"
|
||||
id: hub1_in14
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub1_IN
|
||||
number: 14
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
- platform: gpio
|
||||
name: "hub1_in15"
|
||||
id: hub1_in15
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub1_IN
|
||||
number: 15
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
|
||||
##################
|
||||
#mcp23xxx_hub2_IN#
|
||||
##################
|
||||
|
||||
# Korytarz 2 — single: hub1_out15
|
||||
- platform: gpio
|
||||
name: "hub2_in0"
|
||||
id: hub2_in0
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub2_IN
|
||||
number: 0
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub1_out15
|
||||
|
||||
# Korytarz 3 — single: WLED korytarz kinkiety, long: wyłącz wszystkie
|
||||
- platform: gpio
|
||||
name: "hub2_in1"
|
||||
id: hub2_in1
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub2_IN
|
||||
number: 1
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- script.execute: wled_korytarz_kinkiety_toggle
|
||||
- timing:
|
||||
- ON for at least 350ms
|
||||
then:
|
||||
- script.execute: off_all_lights
|
||||
- script.execute: off_all_fans
|
||||
|
||||
# Korytarz 1 — single: hub1_out14, long: wyłącz wszystkie
|
||||
- platform: gpio
|
||||
name: "hub2_in2"
|
||||
id: hub2_in2
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub2_IN
|
||||
number: 2
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub1_out14
|
||||
- timing:
|
||||
- ON for at least 350ms
|
||||
then:
|
||||
- script.execute: off_all_lights
|
||||
- script.execute: off_all_fans
|
||||
|
||||
# Mały Korytarz — brak automatyzacji w Node-RED
|
||||
- platform: gpio
|
||||
name: "hub2_in3"
|
||||
id: hub2_in3
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub2_IN
|
||||
number: 3
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
|
||||
# Garderoba Mała — single: hub2_out14, double: hub3_out2 (wentylator)
|
||||
- platform: gpio
|
||||
name: "hub2_in4"
|
||||
id: hub2_in4
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub2_IN
|
||||
number: 4
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub2_out14
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub3_out2
|
||||
|
||||
# Kuchnia 3_1 — single: hub1_out8
|
||||
- platform: gpio
|
||||
name: "hub2_in5"
|
||||
id: hub2_in5
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub2_IN
|
||||
number: 5
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub1_out8
|
||||
|
||||
# Kuchnia 3_2 — single: jadalnia LED
|
||||
- platform: gpio
|
||||
name: "hub2_in6"
|
||||
id: hub2_in6
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub2_IN
|
||||
number: 6
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: jadalnia_led
|
||||
|
||||
# Kuchnia 2_1 — single: hub1_out3, triple: wyłącz lampy, long: wyłącz wentylatory
|
||||
- platform: gpio
|
||||
name: "hub2_in7"
|
||||
id: hub2_in7
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub2_IN
|
||||
number: 7
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub1_out3
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- script.execute: off_all_lights
|
||||
- timing:
|
||||
- ON for at least 350ms
|
||||
then:
|
||||
- script.execute: off_all_fans
|
||||
|
||||
# Kuchnia 2_2 — single: kuchnia łezka LED
|
||||
- platform: gpio
|
||||
name: "hub2_in8"
|
||||
id: hub2_in8
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub2_IN
|
||||
number: 8
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: kuchnia_lezka_led
|
||||
|
||||
# Kuchnia 1_1 — single: hub2_out11, double: WLED salon wentylator
|
||||
- platform: gpio
|
||||
name: "hub2_in9"
|
||||
id: hub2_in9
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub2_IN
|
||||
number: 9
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub2_out11
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- script.execute: wled_salon_wentylator_toggle
|
||||
|
||||
# Kuchnia 1_2 — single: WLED komin+salon kinkiety toggle, double: WLED komin on, long: WLED komin on
|
||||
- platform: gpio
|
||||
name: "hub2_in10"
|
||||
id: hub2_in10
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub2_IN
|
||||
number: 10
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- script.execute: wled_komin_toggle
|
||||
- script.execute: wled_salon_kinkiety_toggle
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- script.execute: wled_komin_on
|
||||
- timing:
|
||||
- ON for at least 350ms
|
||||
then:
|
||||
- script.execute: wled_komin_on
|
||||
|
||||
# Schody 3 — single: WLED oczka taras + skrzynia + tunel + zwis schody toggle
|
||||
- platform: gpio
|
||||
name: "hub2_in11"
|
||||
id: hub2_in11
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub2_IN
|
||||
number: 11
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- script.execute: wled_schody_all_toggle
|
||||
|
||||
# Schody 1 — single: hub2_out11
|
||||
- platform: gpio
|
||||
name: "hub2_in12"
|
||||
id: hub2_in12
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub2_IN
|
||||
number: 12
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub2_out11
|
||||
|
||||
# Schody 2 — single: WLED salon kinkiety toggle, double: WLED salon kinkiety off, long: WLED komin toggle
|
||||
- platform: gpio
|
||||
name: "hub2_in13"
|
||||
id: hub2_in13
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub2_IN
|
||||
number: 13
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- script.execute: wled_salon_kinkiety_toggle
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- script.execute: wled_salon_kinkiety_off
|
||||
- timing:
|
||||
- ON for at least 350ms
|
||||
then:
|
||||
- script.execute: wled_komin_toggle
|
||||
|
||||
# Nie podpięte
|
||||
- platform: gpio
|
||||
name: "hub2_in14"
|
||||
id: hub2_in14
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub2_IN
|
||||
number: 14
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
- platform: gpio
|
||||
name: "hub2_in15"
|
||||
id: hub2_in15
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub2_IN
|
||||
number: 15
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
|
||||
##################
|
||||
#mcp23xxx_hub3_IN#
|
||||
##################
|
||||
|
||||
# Garderoba Duża Lewy — single: hub1_out7
|
||||
- platform: gpio
|
||||
name: "hub3_in0"
|
||||
id: hub3_in0
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub3_IN
|
||||
number: 0
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub1_out7
|
||||
|
||||
# Garderoba Duża Prawy — single: hub1_out7
|
||||
- platform: gpio
|
||||
name: "hub3_in1"
|
||||
id: hub3_in1
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub3_IN
|
||||
number: 1
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub1_out7
|
||||
|
||||
# SPA Lewy — single: hub2_out13, triple: hub1_out4 (wentylator)
|
||||
- platform: gpio
|
||||
name: "hub3_in2"
|
||||
id: hub3_in2
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub3_IN
|
||||
number: 2
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub2_out13
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub1_out4
|
||||
|
||||
# SPA Prawy — single: hub1_out4+hub1_out10, triple: SPA Ledy
|
||||
- platform: gpio
|
||||
name: "hub3_in3"
|
||||
id: hub3_in3
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub3_IN
|
||||
number: 3
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub1_out4
|
||||
- light.toggle: hub1_out10
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: spa_ledy
|
||||
|
||||
# Biała Łazienka Ściana — single: hub2_out0, double: biała łazienka wanna LED, long: hub2_out8 (wentylator)
|
||||
- platform: gpio
|
||||
name: "hub3_in4"
|
||||
id: hub3_in4
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub3_IN
|
||||
number: 4
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub2_out0
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: biala_lazienka_wanna_led
|
||||
- timing:
|
||||
- ON for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub2_out8
|
||||
|
||||
# Małe WC Lustro Prawy — bezpośredni toggle hub2_out2 + WLED wc_led + hub2_out5 (wentylator)
|
||||
- platform: gpio
|
||||
name: "hub3_in5"
|
||||
id: hub3_in5
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub3_IN
|
||||
number: 5
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_click:
|
||||
min_length: 50ms
|
||||
max_length: 1000ms
|
||||
then:
|
||||
- light.toggle: hub2_out2
|
||||
- light.toggle: hub2_out5
|
||||
- script.execute: wled_wc_led_toggle
|
||||
|
||||
# Małe WC Lustro Lewy — bezpośredni toggle hub1_out9
|
||||
- platform: gpio
|
||||
name: "hub3_in6"
|
||||
id: hub3_in6
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub3_IN
|
||||
number: 6
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_click:
|
||||
min_length: 50ms
|
||||
max_length: 1000ms
|
||||
then:
|
||||
- light.toggle: hub1_out9
|
||||
|
||||
# Biała Łazienka Skos — single: hub2_out4
|
||||
- platform: gpio
|
||||
name: "hub3_in7"
|
||||
id: hub3_in7
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub3_IN
|
||||
number: 7
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub2_out4
|
||||
|
||||
# SPA Lustro Lewy — single: hub1_out5
|
||||
- platform: gpio
|
||||
name: "hub3_in8"
|
||||
id: hub3_in8
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub3_IN
|
||||
number: 8
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub1_out5
|
||||
|
||||
# SPA Lustro Prawy — single: hub1_out1
|
||||
- platform: gpio
|
||||
name: "hub3_in9"
|
||||
id: hub3_in9
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub3_IN
|
||||
number: 9
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub1_out1
|
||||
|
||||
# Biuro Skos — single: hub1_out2, double: serwer LED wyłącz
|
||||
- platform: gpio
|
||||
name: "hub3_in10"
|
||||
id: hub3_in10
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub3_IN
|
||||
number: 10
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub1_out2
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.turn_off: serwer_led
|
||||
|
||||
# Serwer Prawy — single: serwer LED, double: hub1_out2, triple: hub3_out2 (wentylator)
|
||||
- platform: gpio
|
||||
name: "hub3_in11"
|
||||
id: hub3_in11
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub3_IN
|
||||
number: 11
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: serwer_led
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub1_out2
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub3_out2
|
||||
|
||||
# Serwer Lewy — single: hub1_out14, triple: wyłącz lampy, long: wyłącz wentylatory
|
||||
- platform: gpio
|
||||
name: "hub3_in12"
|
||||
id: hub3_in12
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub3_IN
|
||||
number: 12
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- light.toggle: hub1_out14
|
||||
- timing:
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at most 350ms
|
||||
- ON for at most 350ms
|
||||
- OFF for at least 350ms
|
||||
then:
|
||||
- script.execute: off_all_lights
|
||||
- timing:
|
||||
- ON for at least 350ms
|
||||
then:
|
||||
- script.execute: off_all_fans
|
||||
|
||||
# Nie podpięte
|
||||
- platform: gpio
|
||||
name: "hub3_in13"
|
||||
id: hub3_in13
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub3_IN
|
||||
number: 13
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
- platform: gpio
|
||||
name: "hub3_in14"
|
||||
id: hub3_in14
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub3_IN
|
||||
number: 14
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
- platform: gpio
|
||||
name: "hub3_in15"
|
||||
id: hub3_in15
|
||||
pin:
|
||||
mcp23xxx: mcp23xxx_hub3_IN
|
||||
number: 15
|
||||
mode: INPUT
|
||||
inverted: True
|
||||
407
Mieszkanie Kuby/packages/mcp23017_outputs.yaml
Normal file
407
Mieszkanie Kuby/packages/mcp23017_outputs.yaml
Normal file
@@ -0,0 +1,407 @@
|
||||
mcp23017:
|
||||
- id: "mcp23xxx_hub1_OUT"
|
||||
address: 0x21
|
||||
- id: "mcp23xxx_hub2_OUT"
|
||||
address: 0x23
|
||||
- id: "mcp23xxx_hub3_OUT"
|
||||
address: 0x25
|
||||
|
||||
|
||||
output:
|
||||
# MCP23017 GPIO outputs — hub1
|
||||
- platform: gpio
|
||||
id: hub1_out0_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub1_OUT, number: 0, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub1_out1_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub1_OUT, number: 1, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub1_out2_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub1_OUT, number: 2, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub1_out3_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub1_OUT, number: 3, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub1_out4_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub1_OUT, number: 4, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub1_out5_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub1_OUT, number: 5, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub1_out6_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub1_OUT, number: 6, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub1_out7_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub1_OUT, number: 7, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub1_out8_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub1_OUT, number: 8, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub1_out9_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub1_OUT, number: 9, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub1_out10_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub1_OUT, number: 10, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub1_out11_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub1_OUT, number: 11, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub1_out12_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub1_OUT, number: 12, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub1_out13_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub1_OUT, number: 13, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub1_out14_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub1_OUT, number: 14, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub1_out15_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub1_OUT, number: 15, mode: OUTPUT, inverted: False}
|
||||
|
||||
# MCP23017 GPIO outputs — hub2
|
||||
- platform: gpio
|
||||
id: hub2_out0_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub2_OUT, number: 0, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub2_out1_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub2_OUT, number: 1, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub2_out2_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub2_OUT, number: 2, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub2_out3_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub2_OUT, number: 3, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub2_out4_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub2_OUT, number: 4, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub2_out5_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub2_OUT, number: 5, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub2_out6_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub2_OUT, number: 6, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub2_out7_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub2_OUT, number: 7, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub2_out8_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub2_OUT, number: 8, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub2_out9_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub2_OUT, number: 9, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub2_out10_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub2_OUT, number: 10, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub2_out11_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub2_OUT, number: 11, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub2_out12_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub2_OUT, number: 12, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub2_out13_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub2_OUT, number: 13, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub2_out14_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub2_OUT, number: 14, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub2_out15_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub2_OUT, number: 15, mode: OUTPUT, inverted: False}
|
||||
|
||||
# MCP23017 GPIO outputs — hub3
|
||||
- platform: gpio
|
||||
id: hub3_out0_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub3_OUT, number: 0, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub3_out1_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub3_OUT, number: 1, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub3_out2_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub3_OUT, number: 2, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub3_out3_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub3_OUT, number: 3, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub3_out4_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub3_OUT, number: 4, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub3_out5_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub3_OUT, number: 5, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub3_out6_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub3_OUT, number: 6, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub3_out7_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub3_OUT, number: 7, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub3_out8_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub3_OUT, number: 8, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub3_out9_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub3_OUT, number: 9, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub3_out10_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub3_OUT, number: 10, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub3_out11_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub3_OUT, number: 11, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub3_out12_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub3_OUT, number: 12, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub3_out13_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub3_OUT, number: 13, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub3_out14_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub3_OUT, number: 14, mode: OUTPUT, inverted: False}
|
||||
- platform: gpio
|
||||
id: hub3_out15_gpio
|
||||
pin: {mcp23xxx: mcp23xxx_hub3_OUT, number: 15, mode: OUTPUT, inverted: False}
|
||||
|
||||
|
||||
light:
|
||||
# hub1 outputs
|
||||
- platform: binary
|
||||
name: "hub1_out0"
|
||||
id: hub1_out0
|
||||
output: hub1_out0_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub1_out1"
|
||||
id: hub1_out1
|
||||
output: hub1_out1_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub1_out2"
|
||||
id: hub1_out2
|
||||
output: hub1_out2_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub1_out3"
|
||||
id: hub1_out3
|
||||
output: hub1_out3_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub1_out4" # Fan: SPA/Łazienka
|
||||
id: hub1_out4
|
||||
output: hub1_out4_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub1_out5"
|
||||
id: hub1_out5
|
||||
output: hub1_out5_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub1_out6"
|
||||
id: hub1_out6
|
||||
output: hub1_out6_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub1_out7"
|
||||
id: hub1_out7
|
||||
output: hub1_out7_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub1_out8"
|
||||
id: hub1_out8
|
||||
output: hub1_out8_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub1_out9"
|
||||
id: hub1_out9
|
||||
output: hub1_out9_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub1_out10"
|
||||
id: hub1_out10
|
||||
output: hub1_out10_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub1_out11"
|
||||
id: hub1_out11
|
||||
output: hub1_out11_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub1_out12"
|
||||
id: hub1_out12
|
||||
output: hub1_out12_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub1_out13"
|
||||
id: hub1_out13
|
||||
output: hub1_out13_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub1_out14"
|
||||
id: hub1_out14
|
||||
output: hub1_out14_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub1_out15"
|
||||
id: hub1_out15
|
||||
output: hub1_out15_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
|
||||
# hub2 outputs
|
||||
- platform: binary
|
||||
name: "hub2_out0"
|
||||
id: hub2_out0
|
||||
output: hub2_out0_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub2_out1"
|
||||
id: hub2_out1
|
||||
output: hub2_out1_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub2_out2"
|
||||
id: hub2_out2
|
||||
output: hub2_out2_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub2_out3"
|
||||
id: hub2_out3
|
||||
output: hub2_out3_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub2_out4"
|
||||
id: hub2_out4
|
||||
output: hub2_out4_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub2_out5" # Fan: Małe WC
|
||||
id: hub2_out5
|
||||
output: hub2_out5_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub2_out6"
|
||||
id: hub2_out6
|
||||
output: hub2_out6_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub2_out7"
|
||||
id: hub2_out7
|
||||
output: hub2_out7_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub2_out8" # Fan: Biała Łazienka
|
||||
id: hub2_out8
|
||||
output: hub2_out8_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub2_out9"
|
||||
id: hub2_out9
|
||||
output: hub2_out9_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub2_out10"
|
||||
id: hub2_out10
|
||||
output: hub2_out10_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub2_out11"
|
||||
id: hub2_out11
|
||||
output: hub2_out11_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub2_out12"
|
||||
id: hub2_out12
|
||||
output: hub2_out12_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub2_out13"
|
||||
id: hub2_out13
|
||||
output: hub2_out13_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub2_out14"
|
||||
id: hub2_out14
|
||||
output: hub2_out14_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub2_out15"
|
||||
id: hub2_out15
|
||||
output: hub2_out15_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
|
||||
# hub3 outputs
|
||||
- platform: binary
|
||||
name: "hub3_out0"
|
||||
id: hub3_out0
|
||||
output: hub3_out0_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub3_out1"
|
||||
id: hub3_out1
|
||||
output: hub3_out1_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub3_out2" # Fan: Biuro/Serwer
|
||||
id: hub3_out2
|
||||
output: hub3_out2_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub3_out3"
|
||||
id: hub3_out3
|
||||
output: hub3_out3_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub3_out4"
|
||||
id: hub3_out4
|
||||
output: hub3_out4_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub3_out5"
|
||||
id: hub3_out5
|
||||
output: hub3_out5_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub3_out6"
|
||||
id: hub3_out6
|
||||
output: hub3_out6_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub3_out7"
|
||||
id: hub3_out7
|
||||
output: hub3_out7_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub3_out8"
|
||||
id: hub3_out8
|
||||
output: hub3_out8_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub3_out9"
|
||||
id: hub3_out9
|
||||
output: hub3_out9_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub3_out10"
|
||||
id: hub3_out10
|
||||
output: hub3_out10_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub3_out11"
|
||||
id: hub3_out11
|
||||
output: hub3_out11_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub3_out12"
|
||||
id: hub3_out12
|
||||
output: hub3_out12_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub3_out13"
|
||||
id: hub3_out13
|
||||
output: hub3_out13_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub3_out14"
|
||||
id: hub3_out14
|
||||
output: hub3_out14_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
- platform: binary
|
||||
name: "hub3_out15"
|
||||
id: hub3_out15
|
||||
output: hub3_out15_gpio
|
||||
restore_mode: ALWAYS_OFF
|
||||
221
Mieszkanie Kuby/packages/pca9685.yaml
Normal file
221
Mieszkanie Kuby/packages/pca9685.yaml
Normal file
@@ -0,0 +1,221 @@
|
||||
pca9685:
|
||||
- frequency: 1500
|
||||
address: 0x40
|
||||
id: "pca9685_hub1"
|
||||
- frequency: 1500
|
||||
address: 0x41
|
||||
id: "pca9685_hub2"
|
||||
|
||||
|
||||
output:
|
||||
# Status RGB LED (ledc)
|
||||
- platform: ledc
|
||||
id: output_blue
|
||||
pin: 5
|
||||
- platform: ledc
|
||||
id: output_green
|
||||
pin: 14
|
||||
- platform: ledc
|
||||
id: output_red
|
||||
pin: 15
|
||||
|
||||
# PCA9685 Hub1 — kanały PWM
|
||||
- platform: pca9685
|
||||
id: 'PWM1_1_Hub1'
|
||||
pca9685_id: 'pca9685_hub1'
|
||||
channel: 0
|
||||
max_power: 75%
|
||||
- platform: pca9685
|
||||
id: 'PWM1_2_Hub1'
|
||||
pca9685_id: 'pca9685_hub1'
|
||||
channel: 1
|
||||
max_power: 75%
|
||||
- platform: pca9685
|
||||
id: 'PWM2_1_Hub1'
|
||||
pca9685_id: 'pca9685_hub1'
|
||||
channel: 2
|
||||
max_power: 75%
|
||||
- platform: pca9685
|
||||
id: 'PWM2_2_Hub1'
|
||||
pca9685_id: 'pca9685_hub1'
|
||||
channel: 3
|
||||
max_power: 75%
|
||||
- platform: pca9685
|
||||
id: 'PWM3_1_Hub1'
|
||||
pca9685_id: 'pca9685_hub1'
|
||||
channel: 4
|
||||
max_power: 75%
|
||||
- platform: pca9685
|
||||
id: 'PWM3_2_Hub1'
|
||||
pca9685_id: 'pca9685_hub1'
|
||||
channel: 8
|
||||
max_power: 75%
|
||||
- platform: pca9685
|
||||
id: 'PWM4_1_Hub1'
|
||||
pca9685_id: 'pca9685_hub1'
|
||||
channel: 6
|
||||
min_power: 0%
|
||||
max_power: 75%
|
||||
- platform: pca9685
|
||||
id: 'PWM4_2_Hub1'
|
||||
pca9685_id: 'pca9685_hub1'
|
||||
channel: 7
|
||||
min_power: 0%
|
||||
max_power: 75%
|
||||
- platform: pca9685
|
||||
id: 'PWM5_1_Hub1'
|
||||
pca9685_id: 'pca9685_hub1'
|
||||
channel: 12
|
||||
max_power: 75%
|
||||
- platform: pca9685
|
||||
id: 'PWM5_2_Hub1'
|
||||
pca9685_id: 'pca9685_hub1'
|
||||
channel: 13
|
||||
max_power: 75%
|
||||
|
||||
# PCA9685 Hub2 — kanały PWM
|
||||
- platform: pca9685
|
||||
id: 'PWM1_1_Hub2'
|
||||
pca9685_id: 'pca9685_hub2'
|
||||
channel: 0
|
||||
max_power: 75%
|
||||
- platform: pca9685
|
||||
id: 'PWM1_2_Hub2'
|
||||
pca9685_id: 'pca9685_hub2'
|
||||
channel: 1
|
||||
max_power: 75%
|
||||
- platform: pca9685
|
||||
id: 'PWM2_1_Hub2'
|
||||
pca9685_id: 'pca9685_hub2'
|
||||
channel: 2
|
||||
max_power: 75%
|
||||
- platform: pca9685
|
||||
id: 'PWM2_2_Hub2'
|
||||
pca9685_id: 'pca9685_hub2'
|
||||
channel: 3
|
||||
max_power: 75%
|
||||
- platform: pca9685
|
||||
id: 'PWM3_1_Hub2'
|
||||
pca9685_id: 'pca9685_hub2'
|
||||
channel: 4
|
||||
max_power: 75%
|
||||
- platform: pca9685
|
||||
id: 'PWM3_2_Hub2'
|
||||
pca9685_id: 'pca9685_hub2'
|
||||
channel: 5
|
||||
max_power: 95%
|
||||
- platform: pca9685
|
||||
id: 'PWM4_1_Hub2'
|
||||
pca9685_id: 'pca9685_hub2'
|
||||
channel: 10
|
||||
max_power: 95%
|
||||
- platform: pca9685
|
||||
id: 'PWM4_2_Hub2'
|
||||
pca9685_id: 'pca9685_hub2'
|
||||
channel: 13
|
||||
max_power: 75%
|
||||
- platform: pca9685
|
||||
id: 'PWM5_1_Hub2'
|
||||
pca9685_id: 'pca9685_hub2'
|
||||
channel: 11
|
||||
max_power: 75%
|
||||
- platform: pca9685
|
||||
id: 'PWM5_2_Hub2'
|
||||
pca9685_id: 'pca9685_hub2'
|
||||
channel: 15
|
||||
max_power: 75%
|
||||
|
||||
|
||||
light:
|
||||
- platform: rgb
|
||||
name: "Status LED"
|
||||
red: output_red
|
||||
green: output_green
|
||||
blue: output_blue
|
||||
effects:
|
||||
- pulse:
|
||||
name: "Fast Pulse"
|
||||
transition_length: 0.5s
|
||||
update_interval: 0.5s
|
||||
- pulse:
|
||||
name: "Slow Pulse"
|
||||
transition_length: 1s
|
||||
update_interval: 1s
|
||||
- random:
|
||||
name: "Random Effect"
|
||||
transition_length: 1s
|
||||
update_interval: 2s
|
||||
|
||||
- platform: cwww
|
||||
name: "Serwer LED"
|
||||
id: serwer_led
|
||||
cold_white: PWM2_1_Hub1
|
||||
warm_white: PWM2_2_Hub1
|
||||
cold_white_color_temperature: 6536 K
|
||||
warm_white_color_temperature: 2000 K
|
||||
default_transition_length: 2.0s
|
||||
|
||||
- platform: cwww
|
||||
name: "Jadalnia LED"
|
||||
id: jadalnia_led
|
||||
cold_white: PWM4_2_Hub1
|
||||
warm_white: PWM4_1_Hub1
|
||||
cold_white_color_temperature: 6536 K
|
||||
warm_white_color_temperature: 2000 K
|
||||
default_transition_length: 2.0s
|
||||
|
||||
- platform: cwww
|
||||
name: "Biała Łazienka wanna LED"
|
||||
id: biala_lazienka_wanna_led
|
||||
cold_white: PWM5_2_Hub1
|
||||
warm_white: PWM5_1_Hub1
|
||||
cold_white_color_temperature: 6536 K
|
||||
warm_white_color_temperature: 2000 K
|
||||
default_transition_length: 2.0s
|
||||
|
||||
- platform: monochromatic
|
||||
name: "Kuchnia Łezka LED"
|
||||
id: kuchnia_lezka_led
|
||||
output: PWM2_1_Hub2
|
||||
default_transition_length: 2.0s
|
||||
|
||||
- platform: cwww
|
||||
name: "SPA Ledy"
|
||||
id: spa_ledy
|
||||
cold_white: PWM5_2_Hub2
|
||||
warm_white: PWM5_1_Hub2
|
||||
cold_white_color_temperature: 6536 K
|
||||
warm_white_color_temperature: 2000 K
|
||||
default_transition_length: 2.0s
|
||||
|
||||
- platform: cwww
|
||||
name: "Pokój dla gości LED"
|
||||
id: pokoj_dla_gosci_led
|
||||
cold_white: PWM3_2_Hub2
|
||||
warm_white: PWM4_1_Hub2
|
||||
cold_white_color_temperature: 6536 K
|
||||
warm_white_color_temperature: 2000 K
|
||||
default_transition_length: 2.0s
|
||||
|
||||
- platform: monochromatic
|
||||
name: "TEST LED 1"
|
||||
id: test_led_1
|
||||
output: PWM1_2_Hub2
|
||||
|
||||
- platform: monochromatic
|
||||
name: "TEST LED 2"
|
||||
id: test_led_2
|
||||
output: PWM3_1_Hub2
|
||||
|
||||
- platform: rgbww
|
||||
name: "Sypialnia LED"
|
||||
id: sypialnia_led
|
||||
red: PWM1_1_Hub1
|
||||
green: PWM1_2_Hub1
|
||||
blue: PWM3_1_Hub1
|
||||
cold_white: PWM3_2_Hub1
|
||||
warm_white: PWM1_1_Hub2
|
||||
cold_white_color_temperature: 6536 K
|
||||
warm_white_color_temperature: 2000 K
|
||||
color_interlock: false
|
||||
default_transition_length: 2.0s
|
||||
114
Mieszkanie Kuby/packages/scripts.yaml
Normal file
114
Mieszkanie Kuby/packages/scripts.yaml
Normal file
@@ -0,0 +1,114 @@
|
||||
script:
|
||||
# ── Wyłącz wszystkie lampy ───────────────────────────────────────────────
|
||||
- id: off_all_lights
|
||||
then:
|
||||
- light.turn_off: serwer_led
|
||||
- light.turn_off: jadalnia_led
|
||||
- light.turn_off: biala_lazienka_wanna_led
|
||||
- light.turn_off: kuchnia_lezka_led
|
||||
- light.turn_off: spa_ledy
|
||||
- light.turn_off: pokoj_dla_gosci_led
|
||||
- light.turn_off: test_led_1
|
||||
- light.turn_off: test_led_2
|
||||
- light.turn_off: sypialnia_led
|
||||
- light.turn_off: hub1_out0
|
||||
- light.turn_off: hub1_out1
|
||||
- light.turn_off: hub1_out2
|
||||
- light.turn_off: hub1_out3
|
||||
- light.turn_off: hub1_out5
|
||||
- light.turn_off: hub1_out6
|
||||
- light.turn_off: hub1_out7
|
||||
- light.turn_off: hub1_out8
|
||||
- light.turn_off: hub1_out9
|
||||
- light.turn_off: hub1_out10
|
||||
- light.turn_off: hub1_out11
|
||||
- light.turn_off: hub1_out12
|
||||
- light.turn_off: hub1_out13
|
||||
- light.turn_off: hub1_out14
|
||||
- light.turn_off: hub1_out15
|
||||
- light.turn_off: hub2_out0
|
||||
- light.turn_off: hub2_out1
|
||||
- light.turn_off: hub2_out2
|
||||
- light.turn_off: hub2_out3
|
||||
- light.turn_off: hub2_out4
|
||||
- light.turn_off: hub2_out6
|
||||
- light.turn_off: hub2_out7
|
||||
- light.turn_off: hub2_out9
|
||||
- light.turn_off: hub2_out10
|
||||
- light.turn_off: hub2_out11
|
||||
- light.turn_off: hub2_out12
|
||||
- light.turn_off: hub2_out13
|
||||
- light.turn_off: hub2_out14
|
||||
- light.turn_off: hub2_out15
|
||||
- light.turn_off: hub3_out0
|
||||
- light.turn_off: hub3_out1
|
||||
- light.turn_off: hub3_out3
|
||||
- light.turn_off: hub3_out4
|
||||
- light.turn_off: hub3_out5
|
||||
- light.turn_off: hub3_out6
|
||||
- light.turn_off: hub3_out7
|
||||
- light.turn_off: hub3_out8
|
||||
- light.turn_off: hub3_out9
|
||||
- light.turn_off: hub3_out10
|
||||
- light.turn_off: hub3_out11
|
||||
- light.turn_off: hub3_out12
|
||||
- light.turn_off: hub3_out13
|
||||
- light.turn_off: hub3_out14
|
||||
- light.turn_off: hub3_out15
|
||||
|
||||
# ── Wyłącz wszystkie wentylatory ─────────────────────────────────────────
|
||||
- id: off_all_fans
|
||||
then:
|
||||
- light.turn_off: hub3_out2 # Fan: Biuro/Serwer
|
||||
- light.turn_off: hub1_out4 # Fan: SPA/Łazienka
|
||||
- light.turn_off: hub2_out5 # Fan: Małe WC
|
||||
- light.turn_off: hub2_out8 # Fan: Biała Łazienka
|
||||
|
||||
# ── WLED placeholders (TODO: zastąpić sterowaniem WLED przez HA/MQTT) ───
|
||||
- id: wled_korytarz_kinkiety_toggle
|
||||
then:
|
||||
- logger.log:
|
||||
level: INFO
|
||||
format: "WLED PLACEHOLDER: korytarz_kinkiety toggle"
|
||||
|
||||
- id: wled_komin_toggle
|
||||
then:
|
||||
- logger.log:
|
||||
level: INFO
|
||||
format: "WLED PLACEHOLDER: komin_led toggle"
|
||||
|
||||
- id: wled_komin_on
|
||||
then:
|
||||
- logger.log:
|
||||
level: INFO
|
||||
format: "WLED PLACEHOLDER: komin_led turn_on"
|
||||
|
||||
- id: wled_salon_kinkiety_toggle
|
||||
then:
|
||||
- logger.log:
|
||||
level: INFO
|
||||
format: "WLED PLACEHOLDER: salon_kinkiety_led toggle"
|
||||
|
||||
- id: wled_salon_kinkiety_off
|
||||
then:
|
||||
- logger.log:
|
||||
level: INFO
|
||||
format: "WLED PLACEHOLDER: salon_kinkiety_led turn_off"
|
||||
|
||||
- id: wled_schody_all_toggle
|
||||
then:
|
||||
- logger.log:
|
||||
level: INFO
|
||||
format: "WLED PLACEHOLDER: oczka_taras + skrzynia + tunel + zwis_schody toggle"
|
||||
|
||||
- id: wled_wc_led_toggle
|
||||
then:
|
||||
- logger.log:
|
||||
level: INFO
|
||||
format: "WLED PLACEHOLDER: wc_led toggle"
|
||||
|
||||
- id: wled_salon_wentylator_toggle
|
||||
then:
|
||||
- logger.log:
|
||||
level: INFO
|
||||
format: "WLED PLACEHOLDER: salon_wentylator toggle"
|
||||
@@ -3,6 +3,8 @@ substitutions:
|
||||
friendly_name: 'BoneIO ESP 32x10 Switches'
|
||||
serial_prefix: 'esp' #Don't change it.
|
||||
|
||||
# dodać https://github.com/ethanak/SimplePgSQL i zaczytywac dane z postgressa a nie z Home Assistanta co do karty, pinu i meldunek
|
||||
|
||||
esphome:
|
||||
name: '${name}'
|
||||
friendly_name: '${friendly_name}'
|
||||
|
||||
3
panel-dostepu/.env.example
Normal file
3
panel-dostepu/.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
# Skopiuj do .env i uzupełnij
|
||||
VITE_HA_URL=http://10.20.10.106:8123
|
||||
VITE_APARTMENTS=7
|
||||
224
panel-dostepu/ha_helpers.yaml
Normal file
224
panel-dostepu/ha_helpers.yaml
Normal file
@@ -0,0 +1,224 @@
|
||||
# ============================================================
|
||||
# Home Assistant — helpery dla Panelu Dostępu
|
||||
# ============================================================
|
||||
# Wklej do configuration.yaml lub do osobnych plików przez split config.
|
||||
# Po dodaniu: Ustawienia → Serwer → Uruchom ponownie HA.
|
||||
# ============================================================
|
||||
|
||||
input_number:
|
||||
apartament_1:
|
||||
name: "Apartament 1 — PIN"
|
||||
min: 0
|
||||
max: 99999999
|
||||
step: 1
|
||||
mode: box
|
||||
icon: mdi:lock
|
||||
|
||||
apartament_2:
|
||||
name: "Apartament 2 — PIN"
|
||||
min: 0
|
||||
max: 99999999
|
||||
step: 1
|
||||
mode: box
|
||||
icon: mdi:lock
|
||||
|
||||
apartament_3:
|
||||
name: "Apartament 3 — PIN"
|
||||
min: 0
|
||||
max: 99999999
|
||||
step: 1
|
||||
mode: box
|
||||
icon: mdi:lock
|
||||
|
||||
apartament_4:
|
||||
name: "Apartament 4 — PIN"
|
||||
min: 0
|
||||
max: 99999999
|
||||
step: 1
|
||||
mode: box
|
||||
icon: mdi:lock
|
||||
|
||||
apartament_5:
|
||||
name: "Apartament 5 — PIN"
|
||||
min: 0
|
||||
max: 99999999
|
||||
step: 1
|
||||
mode: box
|
||||
icon: mdi:lock
|
||||
|
||||
apartament_6:
|
||||
name: "Apartament 6 — PIN"
|
||||
min: 0
|
||||
max: 99999999
|
||||
step: 1
|
||||
mode: box
|
||||
icon: mdi:lock
|
||||
|
||||
apartament_7:
|
||||
name: "Apartament 7 — PIN"
|
||||
min: 0
|
||||
max: 99999999
|
||||
step: 1
|
||||
mode: box
|
||||
icon: mdi:lock
|
||||
|
||||
# ============================================================
|
||||
|
||||
input_text:
|
||||
apartament_karta_1:
|
||||
name: "Apartament 1 — Karta"
|
||||
max: 20
|
||||
icon: mdi:card-account-details
|
||||
|
||||
apartament_karta_2:
|
||||
name: "Apartament 2 — Karta"
|
||||
max: 20
|
||||
icon: mdi:card-account-details
|
||||
|
||||
apartament_karta_3:
|
||||
name: "Apartament 3 — Karta"
|
||||
max: 20
|
||||
icon: mdi:card-account-details
|
||||
|
||||
apartament_karta_4:
|
||||
name: "Apartament 4 — Karta"
|
||||
max: 20
|
||||
icon: mdi:card-account-details
|
||||
|
||||
apartament_karta_5:
|
||||
name: "Apartament 5 — Karta"
|
||||
max: 20
|
||||
icon: mdi:card-account-details
|
||||
|
||||
apartament_karta_6:
|
||||
name: "Apartament 6 — Karta"
|
||||
max: 20
|
||||
icon: mdi:card-account-details
|
||||
|
||||
apartament_karta_7:
|
||||
name: "Apartament 7 — Karta"
|
||||
max: 20
|
||||
icon: mdi:card-account-details
|
||||
|
||||
# ============================================================
|
||||
|
||||
input_datetime:
|
||||
apartament_1_zameldowanie:
|
||||
name: "Apartament 1 — Zameldowanie"
|
||||
has_date: true
|
||||
has_time: true
|
||||
icon: mdi:calendar-arrow-right
|
||||
|
||||
apartament_1_wymeldowanie:
|
||||
name: "Apartament 1 — Wymeldowanie"
|
||||
has_date: true
|
||||
has_time: true
|
||||
icon: mdi:calendar-arrow-left
|
||||
|
||||
apartament_2_zameldowanie:
|
||||
name: "Apartament 2 — Zameldowanie"
|
||||
has_date: true
|
||||
has_time: true
|
||||
icon: mdi:calendar-arrow-right
|
||||
|
||||
apartament_2_wymeldowanie:
|
||||
name: "Apartament 2 — Wymeldowanie"
|
||||
has_date: true
|
||||
has_time: true
|
||||
icon: mdi:calendar-arrow-left
|
||||
|
||||
apartament_3_zameldowanie:
|
||||
name: "Apartament 3 — Zameldowanie"
|
||||
has_date: true
|
||||
has_time: true
|
||||
icon: mdi:calendar-arrow-right
|
||||
|
||||
apartament_3_wymeldowanie:
|
||||
name: "Apartament 3 — Wymeldowanie"
|
||||
has_date: true
|
||||
has_time: true
|
||||
icon: mdi:calendar-arrow-left
|
||||
|
||||
apartament_4_zameldowanie:
|
||||
name: "Apartament 4 — Zameldowanie"
|
||||
has_date: true
|
||||
has_time: true
|
||||
icon: mdi:calendar-arrow-right
|
||||
|
||||
apartament_4_wymeldowanie:
|
||||
name: "Apartament 4 — Wymeldowanie"
|
||||
has_date: true
|
||||
has_time: true
|
||||
icon: mdi:calendar-arrow-left
|
||||
|
||||
apartament_5_zameldowanie:
|
||||
name: "Apartament 5 — Zameldowanie"
|
||||
has_date: true
|
||||
has_time: true
|
||||
icon: mdi:calendar-arrow-right
|
||||
|
||||
apartament_5_wymeldowanie:
|
||||
name: "Apartament 5 — Wymeldowanie"
|
||||
has_date: true
|
||||
has_time: true
|
||||
icon: mdi:calendar-arrow-left
|
||||
|
||||
apartament_6_zameldowanie:
|
||||
name: "Apartament 6 — Zameldowanie"
|
||||
has_date: true
|
||||
has_time: true
|
||||
icon: mdi:calendar-arrow-right
|
||||
|
||||
apartament_6_wymeldowanie:
|
||||
name: "Apartament 6 — Wymeldowanie"
|
||||
has_date: true
|
||||
has_time: true
|
||||
icon: mdi:calendar-arrow-left
|
||||
|
||||
apartament_7_zameldowanie:
|
||||
name: "Apartament 7 — Zameldowanie"
|
||||
has_date: true
|
||||
has_time: true
|
||||
icon: mdi:calendar-arrow-right
|
||||
|
||||
apartament_7_wymeldowanie:
|
||||
name: "Apartament 7 — Wymeldowanie"
|
||||
has_date: true
|
||||
has_time: true
|
||||
icon: mdi:calendar-arrow-left
|
||||
|
||||
# ============================================================
|
||||
# input_boolean — flaga "nowy gość" dla automatyzacji świateł
|
||||
# on = gość jeszcze nie wszedł → automatyzacja odpali się przy pierwszym wejściu
|
||||
# off = gość już wszedł → automatyzacja nie odpali się ponownie
|
||||
# Reset przez panel przy meldowaniu nowego gościa.
|
||||
# ============================================================
|
||||
|
||||
input_boolean:
|
||||
apartament_1_nowy_gosc:
|
||||
name: "Apartament 1 — Nowy gość"
|
||||
icon: mdi:account-arrow-right
|
||||
|
||||
apartament_2_nowy_gosc:
|
||||
name: "Apartament 2 — Nowy gość"
|
||||
icon: mdi:account-arrow-right
|
||||
|
||||
apartament_3_nowy_gosc:
|
||||
name: "Apartament 3 — Nowy gość"
|
||||
icon: mdi:account-arrow-right
|
||||
|
||||
apartament_4_nowy_gosc:
|
||||
name: "Apartament 4 — Nowy gość"
|
||||
icon: mdi:account-arrow-right
|
||||
|
||||
apartament_5_nowy_gosc:
|
||||
name: "Apartament 5 — Nowy gość"
|
||||
icon: mdi:account-arrow-right
|
||||
|
||||
apartament_6_nowy_gosc:
|
||||
name: "Apartament 6 — Nowy gość"
|
||||
icon: mdi:account-arrow-right
|
||||
|
||||
apartament_7_nowy_gosc:
|
||||
name: "Apartament 7 — Nowy gość"
|
||||
icon: mdi:account-arrow-right
|
||||
18
panel-dostepu/index.html
Normal file
18
panel-dostepu/index.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Panel Dostępu</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600&family=Inter:wght@300;400;500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1246
panel-dostepu/package-lock.json
generated
Normal file
1246
panel-dostepu/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
panel-dostepu/package.json
Normal file
18
panel-dostepu/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "panel-dostepu",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"start": "npm run build && npm run preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
}
|
||||
129
panel-dostepu/src/App.vue
Normal file
129
panel-dostepu/src/App.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<LoginView v-if="!authed" @authenticated="onAuthenticated" />
|
||||
<template v-else>
|
||||
<DashboardView
|
||||
v-if="selectedApt === null"
|
||||
:token="token"
|
||||
@logout="onLogout"
|
||||
@select="selectedApt = $event"
|
||||
/>
|
||||
<ApartmentView
|
||||
v-else
|
||||
:nr="selectedApt"
|
||||
:token="token"
|
||||
@back="selectedApt = null"
|
||||
@logout="onLogout"
|
||||
/>
|
||||
|
||||
<!-- Ostrzeżenie przed wylogowaniem -->
|
||||
<transition name="fade-warn">
|
||||
<div v-if="showWarning" class="timeout-warning">
|
||||
Brak aktywności — wylogowanie za {{ warningCountdown }} s
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import LoginView from './views/LoginView.vue'
|
||||
import DashboardView from './views/DashboardView.vue'
|
||||
import ApartmentView from './views/ApartmentView.vue'
|
||||
import { loadToken, isAuthed, panelLogout } from './services/auth.js'
|
||||
|
||||
const TIMEOUT_MS = 2 * 60 * 1000 // 2 minuty bezczynności
|
||||
const WARN_BEFORE = 30 * 1000 // ostrzeżenie 30 s przed końcem
|
||||
|
||||
const token = ref(loadToken())
|
||||
const authed = ref(isAuthed())
|
||||
const selectedApt = ref(null)
|
||||
|
||||
// ── Timeout ────────────────────────────────────────────────
|
||||
const showWarning = ref(false)
|
||||
const warningCountdown = ref(30)
|
||||
|
||||
let idleTimer = null
|
||||
let warnTimer = null
|
||||
let countdownInt = null
|
||||
|
||||
function resetTimers() {
|
||||
if (!authed.value) return
|
||||
clearTimeout(idleTimer)
|
||||
clearTimeout(warnTimer)
|
||||
clearInterval(countdownInt)
|
||||
showWarning.value = false
|
||||
|
||||
// Ostrzeżenie po (TIMEOUT_MS - WARN_BEFORE)
|
||||
warnTimer = setTimeout(() => {
|
||||
showWarning.value = true
|
||||
warningCountdown.value = WARN_BEFORE / 1000
|
||||
countdownInt = setInterval(() => {
|
||||
warningCountdown.value--
|
||||
if (warningCountdown.value <= 0) clearInterval(countdownInt)
|
||||
}, 1000)
|
||||
}, TIMEOUT_MS - WARN_BEFORE)
|
||||
|
||||
// Wylogowanie po TIMEOUT_MS
|
||||
idleTimer = setTimeout(() => {
|
||||
onLogout()
|
||||
}, TIMEOUT_MS)
|
||||
}
|
||||
|
||||
function clearAllTimers() {
|
||||
clearTimeout(idleTimer)
|
||||
clearTimeout(warnTimer)
|
||||
clearInterval(countdownInt)
|
||||
showWarning.value = false
|
||||
}
|
||||
|
||||
const EVENTS = ['mousemove', 'mousedown', 'keydown', 'touchstart', 'scroll', 'click']
|
||||
|
||||
function bindEvents() { EVENTS.forEach(e => window.addEventListener(e, resetTimers, { passive: true })) }
|
||||
function unbindEvents() { EVENTS.forEach(e => window.removeEventListener(e, resetTimers)) }
|
||||
|
||||
// Uruchom/zatrzymaj timer gdy zmienia się stan logowania
|
||||
watch(authed, (val) => {
|
||||
if (val) { bindEvents(); resetTimers() }
|
||||
else { unbindEvents(); clearAllTimers() }
|
||||
}, { immediate: true })
|
||||
|
||||
onUnmounted(() => { unbindEvents(); clearAllTimers() })
|
||||
|
||||
// ── Auth ───────────────────────────────────────────────────
|
||||
function onAuthenticated() {
|
||||
authed.value = true
|
||||
}
|
||||
|
||||
function onLogout() {
|
||||
panelLogout()
|
||||
authed.value = false
|
||||
selectedApt.value = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.timeout-warning {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #7a2a2a;
|
||||
color: #f0d0d0;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 13px;
|
||||
padding: 10px 24px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(200, 100, 100, 0.3);
|
||||
z-index: 9999;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.fade-warn-enter-active, .fade-warn-leave-active {
|
||||
transition: opacity 0.4s ease, transform 0.4s ease;
|
||||
}
|
||||
.fade-warn-enter-from, .fade-warn-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(12px);
|
||||
}
|
||||
</style>
|
||||
5
panel-dostepu/src/main.js
Normal file
5
panel-dostepu/src/main.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import './style.css'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
46
panel-dostepu/src/services/auth.js
Normal file
46
panel-dostepu/src/services/auth.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const STORAGE_KEY = 'ha_token'
|
||||
const SESSION_KEY = 'panel_authed'
|
||||
const PRESET_TOKEN = import.meta.env.VITE_HA_TOKEN || ''
|
||||
const VALID_LOGIN = import.meta.env.VITE_LOGIN || ''
|
||||
const VALID_PASS = import.meta.env.VITE_PASSWORD || ''
|
||||
|
||||
export function loadToken() {
|
||||
if (PRESET_TOKEN) return PRESET_TOKEN
|
||||
return localStorage.getItem(STORAGE_KEY) || ''
|
||||
}
|
||||
|
||||
export function saveToken(token) {
|
||||
localStorage.setItem(STORAGE_KEY, token)
|
||||
}
|
||||
|
||||
export function clearToken() {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
}
|
||||
|
||||
/** Sprawdza czy użytkownik jest zalogowany do panelu */
|
||||
export function isAuthed() {
|
||||
return sessionStorage.getItem(SESSION_KEY) === '1'
|
||||
}
|
||||
|
||||
/**
|
||||
* Loguje do panelu (login/hasło z .env).
|
||||
* Zwraca true jeśli OK, false jeśli błędne dane.
|
||||
*/
|
||||
export function panelLogin(username, password) {
|
||||
if (username === VALID_LOGIN && password === VALID_PASS) {
|
||||
sessionStorage.setItem(SESSION_KEY, '1')
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function panelLogout() {
|
||||
sessionStorage.removeItem(SESSION_KEY)
|
||||
}
|
||||
|
||||
export async function validateToken(token) {
|
||||
const res = await fetch('/api/', {
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
})
|
||||
return res.ok
|
||||
}
|
||||
128
panel-dostepu/src/services/ha.js
Normal file
128
panel-dostepu/src/services/ha.js
Normal file
@@ -0,0 +1,128 @@
|
||||
async function callService(token, domain, service, data) {
|
||||
const res = await fetch(`/api/services/${domain}/${service}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '')
|
||||
throw new Error(`HTTP ${res.status}${text ? ': ' + text : ''}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function setPin(token, nr, pin) {
|
||||
const value = Number(pin)
|
||||
if (isNaN(value)) throw new Error('PIN musi być liczbą')
|
||||
await callService(token, 'input_number', 'set_value', {
|
||||
entity_id: `input_number.apartament_${nr}`,
|
||||
value,
|
||||
})
|
||||
}
|
||||
|
||||
export async function setCard(token, nr, cardId) {
|
||||
if (!cardId.trim()) throw new Error('Numer karty nie może być pusty')
|
||||
await callService(token, 'input_text', 'set_value', {
|
||||
entity_id: `input_text.apartament_karta_${nr}`,
|
||||
value: cardId.trim(),
|
||||
})
|
||||
}
|
||||
|
||||
export async function setCheckin(token, nr, datetimeLocal) {
|
||||
await callService(token, 'input_datetime', 'set_datetime', {
|
||||
entity_id: `input_datetime.apartament_${nr}_zameldowanie`,
|
||||
datetime: datetimeLocal.replace('T', ' ') + ':00',
|
||||
})
|
||||
}
|
||||
|
||||
export async function setCheckout(token, nr, datetimeLocal) {
|
||||
await callService(token, 'input_datetime', 'set_datetime', {
|
||||
entity_id: `input_datetime.apartament_${nr}_wymeldowanie`,
|
||||
datetime: datetimeLocal.replace('T', ' ') + ':00',
|
||||
})
|
||||
}
|
||||
|
||||
async function getEntityData(token, entityId) {
|
||||
const res = await fetch(`/api/states/${entityId}`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
})
|
||||
if (!res.ok) throw new Error(`Nie można odczytać ${entityId} (HTTP ${res.status})`)
|
||||
return await res.json()
|
||||
}
|
||||
|
||||
async function getState(token, entityId) {
|
||||
return (await getEntityData(token, entityId)).state
|
||||
}
|
||||
|
||||
/**
|
||||
* Ustawia flagę "nowy gość" (input_boolean.apartament_X_nowy_gosc).
|
||||
* turn_on = nowy gość (automatyzacja aktywna)
|
||||
* turn_off = reset (po pierwszym wejściu HA samo wywoła turn_off)
|
||||
*/
|
||||
export async function setNewGuest(token, nr, value = true) {
|
||||
const service = value ? 'turn_on' : 'turn_off'
|
||||
await callService(token, 'input_boolean', service, {
|
||||
entity_id: `input_boolean.apartament_${nr}_nowy_gosc`,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Zwraca datę ostatniej zmiany wartości encji (last_changed).
|
||||
* @returns {Date|null}
|
||||
*/
|
||||
export async function getLastChanged(token, entityId) {
|
||||
try {
|
||||
const data = await getEntityData(token, entityId)
|
||||
return data.last_changed ? new Date(data.last_changed) : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyAll(token, nr, { checkin, checkout, pin, card }) {
|
||||
const errors = []
|
||||
const normalize = dt => dt.replace('T', ' ') + ':00'
|
||||
|
||||
const checks = []
|
||||
|
||||
if (checkin !== null) {
|
||||
checks.push(
|
||||
getState(token, `input_datetime.apartament_${nr}_zameldowanie`).then(s => {
|
||||
if (s !== normalize(checkin))
|
||||
errors.push(`Zameldowanie: HA ma "${s}", oczekiwano "${normalize(checkin)}"`)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (checkout !== null) {
|
||||
checks.push(
|
||||
getState(token, `input_datetime.apartament_${nr}_wymeldowanie`).then(s => {
|
||||
if (s !== normalize(checkout))
|
||||
errors.push(`Wymeldowanie: HA ma "${s}", oczekiwano "${normalize(checkout)}"`)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (pin !== null) {
|
||||
checks.push(
|
||||
getState(token, `input_number.apartament_${nr}`).then(s => {
|
||||
if (Number(s) !== Number(pin))
|
||||
errors.push(`PIN: HA ma "${s}", oczekiwano "${pin}"`)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (card !== null) {
|
||||
checks.push(
|
||||
getState(token, `input_text.apartament_karta_${nr}`).then(s => {
|
||||
if (s !== card.trim())
|
||||
errors.push(`Karta: HA ma "${s}", oczekiwano "${card.trim()}"`)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all(checks)
|
||||
return errors
|
||||
}
|
||||
148
panel-dostepu/src/style.css
Normal file
148
panel-dostepu/src/style.css
Normal file
@@ -0,0 +1,148 @@
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg: #0c0c0c;
|
||||
--bg-card: #161616;
|
||||
--bg-input: #1e1e1e;
|
||||
--border: rgba(255, 255, 255, 0.07);
|
||||
--border-focus: rgba(185, 150, 60, 0.5);
|
||||
--gold: #b9963c;
|
||||
--gold-light: #d4af6a;
|
||||
--text: #ede9e3;
|
||||
--text-muted: #6b6560;
|
||||
--text-dim: #9e9890;
|
||||
--success: #4a7c59;
|
||||
--success-bg: rgba(74, 124, 89, 0.12);
|
||||
--error: #8b3a3a;
|
||||
--error-bg: rgba(139, 58, 58, 0.12);
|
||||
--font-serif: 'Playfair Display', Georgia, serif;
|
||||
--font-sans: 'Inter', system-ui, sans-serif;
|
||||
--radius: 4px;
|
||||
--radius-lg: 8px;
|
||||
--transition: 200ms ease;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ── Typography ─────────────────────────────────────────── */
|
||||
h1, h2, h3 {
|
||||
font-family: var(--font-serif);
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* ── Inputs ─────────────────────────────────────────────── */
|
||||
input {
|
||||
width: 100%;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--text);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 14px;
|
||||
padding: 10px 14px;
|
||||
outline: none;
|
||||
transition: border-color var(--transition), box-shadow var(--transition);
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-color: var(--border-focus);
|
||||
box-shadow: 0 0 0 3px rgba(185, 150, 60, 0.08);
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ── Buttons ─────────────────────────────────────────────── */
|
||||
button {
|
||||
cursor: pointer;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
padding: 10px 20px;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--gold);
|
||||
color: #0c0c0c;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--gold-light);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--text-dim);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
border-color: rgba(255,255,255,0.15);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ── Divider ─────────────────────────────────────────────── */
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* ── Feedback ────────────────────────────────────────────── */
|
||||
.feedback {
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 13px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.feedback.success {
|
||||
background: var(--success-bg);
|
||||
color: #7eba94;
|
||||
border: 1px solid rgba(74, 124, 89, 0.25);
|
||||
}
|
||||
|
||||
.feedback.error {
|
||||
background: var(--error-bg);
|
||||
color: #c97a7a;
|
||||
border: 1px solid rgba(139, 58, 58, 0.25);
|
||||
}
|
||||
|
||||
/* ── Scrollbar ───────────────────────────────────────────── */
|
||||
::-webkit-scrollbar { width: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #2a2a2a; border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #3a3a3a; }
|
||||
763
panel-dostepu/src/views/ApartmentView copy.vue
Normal file
763
panel-dostepu/src/views/ApartmentView copy.vue
Normal file
@@ -0,0 +1,763 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<header class="topbar">
|
||||
<div class="topbar-inner">
|
||||
<div class="brand">
|
||||
<span class="brand-mark">■</span>
|
||||
<span class="brand-name">Panel Dostępu</span>
|
||||
</div>
|
||||
<button class="btn-ghost logout-btn" @click="$emit('logout')">Wyloguj</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="content">
|
||||
<button class="back-btn" @click="$emit('back')">
|
||||
<span class="back-arrow">←</span> Powrót do listy
|
||||
</button>
|
||||
|
||||
<div class="apt-heading">
|
||||
<div class="apt-badge">{{ nr }}</div>
|
||||
<h2>Apartament {{ nr }}</h2>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- ── Dostęp ─────────────────────────────────────────── -->
|
||||
<section class="card">
|
||||
|
||||
<h3 class="card-title">Dane dostępu</h3>
|
||||
|
||||
<div class="field">
|
||||
<label>
|
||||
<span class="label-dot" style="color:var(--gold)">●</span>
|
||||
Kod PIN
|
||||
</label>
|
||||
<div class="input-row">
|
||||
<input
|
||||
v-model="pin"
|
||||
type="password"
|
||||
placeholder="Nowy kod PIN"
|
||||
maxlength="8"
|
||||
autocomplete="new-password"
|
||||
:disabled="pinLoading"
|
||||
@keydown.enter="submitPin"
|
||||
/>
|
||||
<button
|
||||
class="btn-primary action-btn"
|
||||
:disabled="!pin || pinLoading"
|
||||
@click="submitPin"
|
||||
>
|
||||
<span v-if="pinLoading" class="spinner" />
|
||||
<span v-else>Ustaw</span>
|
||||
</button>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<div v-if="pinFeedback" :class="['feedback', pinFeedback.type]">
|
||||
{{ pinFeedback.msg }}
|
||||
</div>
|
||||
</transition>
|
||||
<div class="last-changed">Ostatnia zmiana: {{ formatDate(pinLastChanged) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="divider" />
|
||||
|
||||
<div class="field">
|
||||
<label>
|
||||
<span class="label-dot" style="color:var(--gold)">■</span>
|
||||
Karta zbliżeniowa
|
||||
</label>
|
||||
<div class="input-row">
|
||||
<input
|
||||
v-model="card"
|
||||
type="text"
|
||||
placeholder="Numer karty (np. 2438620732)"
|
||||
maxlength="20"
|
||||
autocomplete="off"
|
||||
:disabled="cardLoading"
|
||||
@keydown.enter="submitCard"
|
||||
/>
|
||||
<button
|
||||
class="btn-primary action-btn"
|
||||
:disabled="!card || cardLoading"
|
||||
@click="submitCard"
|
||||
>
|
||||
<span v-if="cardLoading" class="spinner" />
|
||||
<span v-else>Ustaw</span>
|
||||
</button>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<div v-if="cardFeedback" :class="['feedback', cardFeedback.type]">
|
||||
{{ cardFeedback.msg }}
|
||||
</div>
|
||||
</transition>
|
||||
<div class="last-changed">Ostatnia zmiana: {{ formatDate(cardLastChanged) }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Daty meldunku ─────────────────────────────────── -->
|
||||
<section class="card">
|
||||
<div class="card-title-row">
|
||||
<h3 class="card-title">Meldunek</h3>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="updateDates" />
|
||||
<span class="checkbox-custom" />
|
||||
<span>Aktualizuj czas meldunku</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div :class="['dates-grid', { 'dates-disabled': !updateDates }]">
|
||||
<div class="field">
|
||||
<label>Zameldowanie</label>
|
||||
<input type="datetime-local" v-model="checkin" :disabled="!updateDates" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Wymeldowanie</label>
|
||||
<input type="datetime-local" v-model="checkout" :disabled="!updateDates" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-if="datesFeedback" :class="['feedback', datesFeedback.type]">
|
||||
{{ datesFeedback.msg }}
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<div class="card-actions">
|
||||
<button
|
||||
class="btn-primary"
|
||||
:disabled="datesLoading || !updateDates"
|
||||
@click="submitDates"
|
||||
>
|
||||
<span v-if="datesLoading" class="spinner" />
|
||||
<span v-else>Zapisz daty meldunku</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Główne przyciski ──────────────────────────────── -->
|
||||
<transition name="fade">
|
||||
<div v-if="allFeedback" :class="['feedback', 'feedback-main', allFeedback.type]">
|
||||
{{ allFeedback.msg }}
|
||||
</div>
|
||||
</transition>
|
||||
</main>
|
||||
|
||||
<div class="bottom-bar">
|
||||
<div class="bottom-bar-inner">
|
||||
<button class="btn-ghost btn-lg" @click="$emit('back')">
|
||||
← Cofnij
|
||||
</button>
|
||||
<button
|
||||
class="btn-primary btn-lg"
|
||||
:disabled="saveAllLoading"
|
||||
@click="openModeDialog"
|
||||
>
|
||||
<span v-if="saveAllLoading" class="spinner spinner-dark" />
|
||||
<span v-else>Zapisz wszystko</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Modal: tryb zapisu ──────────────────────────────── -->
|
||||
<transition name="modal">
|
||||
<div v-if="showModeDialog" class="modal-backdrop" @click.self="showModeDialog = false">
|
||||
<div class="modal-box">
|
||||
<p class="modal-question">Rodzaj operacji dla apartamentu {{ nr }}?</p>
|
||||
<div class="modal-choices">
|
||||
<button class="modal-choice" @click="confirmMode('new')">
|
||||
<span class="choice-icon">○</span>
|
||||
<span class="choice-label">Nowy gość</span>
|
||||
<span class="choice-desc">Zapisz dane i aktywuj automatyzację świateł przy pierwszym wejściu</span>
|
||||
</button>
|
||||
<button class="modal-choice" @click="confirmMode('reset')">
|
||||
<span class="choice-icon">■</span>
|
||||
<span class="choice-label">Reset PIN / karty</span>
|
||||
<span class="choice-desc">Tylko aktualizacja danych dostępu, bez zmiany automatyzacji</span>
|
||||
</button>
|
||||
</div>
|
||||
<button class="modal-cancel" @click="showModeDialog = false">Anuluj</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { setPin, setCard, setCheckin, setCheckout, verifyAll, getLastChanged, setNewGuest } from '../services/ha.js'
|
||||
|
||||
const props = defineProps({
|
||||
nr: { type: Number, required: true },
|
||||
token: { type: String, required: true },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['back', 'logout'])
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────
|
||||
function pad(n) { return String(n).padStart(2, '0') }
|
||||
|
||||
function toDatetimeLocal(date) {
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`
|
||||
}
|
||||
|
||||
function defaultCheckin() {
|
||||
return toDatetimeLocal(new Date())
|
||||
}
|
||||
|
||||
function defaultCheckout() {
|
||||
const d = new Date()
|
||||
d.setDate(d.getDate() + 1)
|
||||
d.setHours(11, 0, 0, 0)
|
||||
return toDatetimeLocal(d)
|
||||
}
|
||||
|
||||
const FMT = new Intl.DateTimeFormat('pl-PL', {
|
||||
day: '2-digit', month: 'short', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit',
|
||||
})
|
||||
|
||||
function formatDate(date) {
|
||||
if (!date) return '—'
|
||||
return FMT.format(date)
|
||||
}
|
||||
|
||||
function showFeedback(target, type, msg, duration = 3500) {
|
||||
target.value = { type, msg }
|
||||
setTimeout(() => { target.value = null }, duration)
|
||||
}
|
||||
|
||||
// ── State ──────────────────────────────────────────────────
|
||||
const checkin = ref(defaultCheckin())
|
||||
const checkout = ref(defaultCheckout())
|
||||
const datesLoading = ref(false)
|
||||
const datesFeedback = ref(null)
|
||||
|
||||
const pin = ref('')
|
||||
const pinLoading = ref(false)
|
||||
const pinFeedback = ref(null)
|
||||
|
||||
const card = ref('')
|
||||
const cardLoading = ref(false)
|
||||
const cardFeedback = ref(null)
|
||||
|
||||
const updateDates = ref(true)
|
||||
|
||||
const saveAllLoading = ref(false)
|
||||
const allFeedback = ref(null)
|
||||
const showModeDialog = ref(false)
|
||||
|
||||
const pinLastChanged = ref(null)
|
||||
const cardLastChanged = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
const [pinDate, cardDate] = await Promise.all([
|
||||
getLastChanged(props.token, `input_number.apartament_${props.nr}`),
|
||||
getLastChanged(props.token, `input_text.apartament_karta_${props.nr}`),
|
||||
])
|
||||
pinLastChanged.value = pinDate
|
||||
cardLastChanged.value = cardDate
|
||||
})
|
||||
|
||||
// ── Dialog ─────────────────────────────────────────────────
|
||||
function openModeDialog() {
|
||||
showModeDialog.value = true
|
||||
}
|
||||
|
||||
async function confirmMode(mode) {
|
||||
showModeDialog.value = false
|
||||
await submitAll(mode)
|
||||
}
|
||||
|
||||
// ── Actions ────────────────────────────────────────────────
|
||||
async function submitAll(mode) {
|
||||
saveAllLoading.value = true
|
||||
allFeedback.value = null
|
||||
|
||||
const pinToSave = pin.value || null
|
||||
const cardToSave = card.value || null
|
||||
|
||||
try {
|
||||
// 1. Zapis do HA
|
||||
const writes = []
|
||||
if (updateDates.value) {
|
||||
writes.push(setCheckin(props.token, props.nr, checkin.value))
|
||||
writes.push(setCheckout(props.token, props.nr, checkout.value))
|
||||
}
|
||||
if (pinToSave) writes.push(setPin(props.token, props.nr, pinToSave))
|
||||
if (cardToSave) writes.push(setCard(props.token, props.nr, cardToSave))
|
||||
if (mode === 'new') writes.push(setNewGuest(props.token, props.nr, true))
|
||||
await Promise.all(writes)
|
||||
|
||||
// 2. Krótka chwila — HA potrzebuje ~200ms na aktualizację stanu
|
||||
await new Promise(r => setTimeout(r, 300))
|
||||
|
||||
// 3. Weryfikacja — odczyt z HA i porównanie
|
||||
const errors = await verifyAll(props.token, props.nr, {
|
||||
checkin: updateDates.value ? checkin.value : null,
|
||||
checkout: updateDates.value ? checkout.value : null,
|
||||
pin: pinToSave,
|
||||
card: cardToSave,
|
||||
})
|
||||
|
||||
if (errors.length === 0) {
|
||||
const now = new Date()
|
||||
if (pinToSave) pinLastChanged.value = now
|
||||
if (cardToSave) cardLastChanged.value = now
|
||||
pin.value = ''
|
||||
card.value = ''
|
||||
const modeMsg = mode === 'new' ? ' Flaga nowego gościa aktywowana.' : ''
|
||||
showFeedback(allFeedback, 'success', `Zapisano i zweryfikowano — wszystkie dane są poprawne.${modeMsg}`)
|
||||
} else {
|
||||
showFeedback(
|
||||
allFeedback,
|
||||
'error',
|
||||
'Zapis częściowo nieudany:\n' + errors.join('\n'),
|
||||
6000
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
showFeedback(allFeedback, 'error', `Błąd: ${err.message}`)
|
||||
} finally {
|
||||
saveAllLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitDates() {
|
||||
datesLoading.value = true
|
||||
try {
|
||||
await Promise.all([
|
||||
setCheckin(props.token, props.nr, checkin.value),
|
||||
setCheckout(props.token, props.nr, checkout.value),
|
||||
])
|
||||
showFeedback(datesFeedback, 'success', 'Daty meldunku zostały zapisane.')
|
||||
} catch (err) {
|
||||
showFeedback(datesFeedback, 'error', `Błąd: ${err.message}`)
|
||||
} finally {
|
||||
datesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitPin() {
|
||||
if (!pin.value || pinLoading.value) return
|
||||
pinLoading.value = true
|
||||
try {
|
||||
await setPin(props.token, props.nr, pin.value)
|
||||
pin.value = ''
|
||||
pinLastChanged.value = new Date()
|
||||
showFeedback(pinFeedback, 'success', 'Kod PIN został zaktualizowany.')
|
||||
} catch (err) {
|
||||
showFeedback(pinFeedback, 'error', `Błąd: ${err.message}`)
|
||||
} finally {
|
||||
pinLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitCard() {
|
||||
if (!card.value || cardLoading.value) return
|
||||
cardLoading.value = true
|
||||
try {
|
||||
await setCard(props.token, props.nr, card.value)
|
||||
card.value = ''
|
||||
cardLastChanged.value = new Date()
|
||||
showFeedback(cardFeedback, 'success', 'Numer karty został zaktualizowany.')
|
||||
} catch (err) {
|
||||
showFeedback(cardFeedback, 'error', `Błąd: ${err.message}`)
|
||||
} finally {
|
||||
cardLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 80%, rgba(185, 150, 60, 0.03) 0%, transparent 55%),
|
||||
var(--bg);
|
||||
}
|
||||
|
||||
/* ── Topbar ───────────────────────────────────────────────── */
|
||||
.topbar {
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: rgba(12, 12, 12, 0.92);
|
||||
backdrop-filter: blur(12px);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.topbar-inner {
|
||||
max-width: 680px;
|
||||
margin: 0 auto;
|
||||
padding: 0 32px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.brand { display: flex; align-items: center; gap: 10px; }
|
||||
.brand-mark { color: var(--gold); font-size: 9px; }
|
||||
.brand-name { font-family: var(--font-serif); font-size: 17px; letter-spacing: 0.03em; }
|
||||
.logout-btn { font-size: 12px; padding: 7px 16px; }
|
||||
|
||||
/* ── Content ─────────────────────────────────────────────── */
|
||||
.content {
|
||||
max-width: 680px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
padding: 36px 32px 60px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 32px;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
transition: color var(--transition);
|
||||
}
|
||||
|
||||
.back-btn:hover { color: var(--text); }
|
||||
.back-arrow { font-size: 16px; }
|
||||
|
||||
.apt-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.apt-badge {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border: 1px solid var(--gold);
|
||||
color: var(--gold);
|
||||
font-family: var(--font-serif);
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.apt-heading h2 {
|
||||
font-size: 26px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ── Checkbox ────────────────────────────────────────────── */
|
||||
.card-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.card-title-row .card-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.checkbox-custom {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: border-color var(--transition), background var(--transition);
|
||||
}
|
||||
|
||||
.checkbox-label input:checked + .checkbox-custom {
|
||||
background: var(--gold);
|
||||
border-color: var(--gold);
|
||||
}
|
||||
|
||||
.checkbox-label input:checked + .checkbox-custom::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 4px;
|
||||
height: 7px;
|
||||
border: 2px solid #000;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
transform: rotate(45deg) translate(-1px, -1px);
|
||||
}
|
||||
|
||||
.checkbox-label:hover .checkbox-custom {
|
||||
border-color: var(--gold);
|
||||
}
|
||||
|
||||
.dates-disabled {
|
||||
opacity: 0.35;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ── Cards ───────────────────────────────────────────────── */
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 28px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 22px;
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* ── Dates ───────────────────────────────────────────────── */
|
||||
.dates-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.field { margin-bottom: 4px; }
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.label-dot { font-size: 7px; }
|
||||
|
||||
/* Style datetime-local jak zwykły input */
|
||||
input[type="datetime-local"] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.card-actions .btn-primary {
|
||||
padding: 11px 24px;
|
||||
}
|
||||
|
||||
/* ── Access fields ───────────────────────────────────────── */
|
||||
.input-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.input-row input { flex: 1; }
|
||||
|
||||
.action-btn {
|
||||
padding: 10px 16px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Spinner ─────────────────────────────────────────────── */
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid rgba(0,0,0,0.3);
|
||||
border-top-color: #000;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.last-changed {
|
||||
margin-top: 7px;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.25s ease, transform 0.25s ease; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; transform: translateY(-4px); }
|
||||
|
||||
/* ── Bottom bar ──────────────────────────────────────────── */
|
||||
.bottom-bar {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
border-top: 1px solid var(--border);
|
||||
background: rgba(12, 12, 12, 0.95);
|
||||
backdrop-filter: blur(12px);
|
||||
z-index: 100;
|
||||
padding: 16px 32px;
|
||||
}
|
||||
|
||||
.bottom-bar-inner {
|
||||
max-width: 680px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
flex: 1;
|
||||
padding: 15px 24px;
|
||||
font-size: 14px;
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.spinner-dark {
|
||||
border-color: rgba(0,0,0,0.25);
|
||||
border-top-color: #000;
|
||||
}
|
||||
|
||||
.feedback-main {
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
/* ── Modal ───────────────────────────────────────────────── */
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
z-index: 200;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.modal-box {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 32px 28px 24px;
|
||||
width: 100%;
|
||||
max-width: 460px;
|
||||
}
|
||||
|
||||
.modal-question {
|
||||
font-family: var(--font-serif);
|
||||
font-size: 19px;
|
||||
color: var(--text);
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-choices {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.modal-choice {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 16px 20px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
display: grid;
|
||||
grid-template-columns: 20px 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
column-gap: 12px;
|
||||
row-gap: 4px;
|
||||
transition: background var(--transition), border-color var(--transition);
|
||||
}
|
||||
|
||||
.modal-choice:hover {
|
||||
background: rgba(185, 150, 60, 0.08);
|
||||
border-color: rgba(185, 150, 60, 0.4);
|
||||
}
|
||||
|
||||
.choice-icon {
|
||||
grid-row: 1 / 3;
|
||||
align-self: center;
|
||||
color: var(--gold);
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.choice-label {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 14px;
|
||||
color: var(--text);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.choice-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.modal-cancel {
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 13px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
transition: color var(--transition);
|
||||
}
|
||||
|
||||
.modal-cancel:hover { color: var(--text); }
|
||||
|
||||
/* Modal transition */
|
||||
.modal-enter-active, .modal-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.modal-enter-active .modal-box, .modal-leave-active .modal-box {
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
.modal-enter-from, .modal-leave-to { opacity: 0; }
|
||||
.modal-enter-from .modal-box, .modal-leave-to .modal-box {
|
||||
transform: translateY(12px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.topbar-inner, .content { padding-left: 16px; padding-right: 16px; }
|
||||
.dates-grid { grid-template-columns: 1fr; }
|
||||
.bottom-bar { padding: 12px 16px; }
|
||||
}
|
||||
</style>
|
||||
730
panel-dostepu/src/views/ApartmentView.vue
Normal file
730
panel-dostepu/src/views/ApartmentView.vue
Normal file
@@ -0,0 +1,730 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<header class="topbar">
|
||||
<div class="topbar-inner">
|
||||
<div class="brand">
|
||||
<span class="brand-mark">■</span>
|
||||
<span class="brand-name">Panel Dostępu</span>
|
||||
</div>
|
||||
<button class="btn-ghost logout-btn" @click="$emit('logout')">Wyloguj</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="content">
|
||||
<button class="back-btn" @click="$emit('back')">
|
||||
<span class="back-arrow">←</span> Powrót do listy
|
||||
</button>
|
||||
|
||||
<div class="apt-heading">
|
||||
<div class="apt-badge">{{ nr }}</div>
|
||||
<h2>Apartament {{ nr }}</h2>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- ── Dostęp ─────────────────────────────────────────── -->
|
||||
<section class="card">
|
||||
|
||||
|
||||
|
||||
<div class="field">
|
||||
<label>
|
||||
<span class="label-dot" style="color:var(--gold)">■</span>
|
||||
Karta zbliżeniowa
|
||||
</label>
|
||||
<div class="input-row">
|
||||
<input
|
||||
v-model="card"
|
||||
|
||||
type="password"
|
||||
placeholder="Numer karty (np. 2438620732)"
|
||||
maxlength="20"
|
||||
autocomplete="off"
|
||||
:disabled="cardLoading"
|
||||
@keydown.enter="submitCard"
|
||||
/>
|
||||
<button
|
||||
class="btn-primary action-btn"
|
||||
:disabled="!card || cardLoading"
|
||||
@click="submitCard"
|
||||
>
|
||||
<span v-if="cardLoading" class="spinner" />
|
||||
<span v-else>Ustaw</span>
|
||||
</button>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<div v-if="cardFeedback" :class="['feedback', cardFeedback.type]">
|
||||
{{ cardFeedback.msg }}
|
||||
</div>
|
||||
</transition>
|
||||
<div class="last-changed">Ostatnia zmiana: {{ formatDate(cardLastChanged) }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Daty meldunku ─────────────────────────────────── -->
|
||||
<section class="card">
|
||||
<div class="card-title-row">
|
||||
<h3 class="card-title">Meldunek</h3>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="updateDates" />
|
||||
<span class="checkbox-custom" />
|
||||
<span>Aktualizuj czas meldunku</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div :class="['dates-grid', { 'dates-disabled': !updateDates }]">
|
||||
<div class="field">
|
||||
<label>Zameldowanie</label>
|
||||
<input type="datetime-local" v-model="checkin" :disabled="!updateDates" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Wymeldowanie</label>
|
||||
<input type="datetime-local" v-model="checkout" :disabled="!updateDates" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-if="datesFeedback" :class="['feedback', datesFeedback.type]">
|
||||
{{ datesFeedback.msg }}
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<div class="card-actions">
|
||||
<button
|
||||
class="btn-primary"
|
||||
:disabled="datesLoading || !updateDates"
|
||||
@click="submitDates"
|
||||
>
|
||||
<span v-if="datesLoading" class="spinner" />
|
||||
<span v-else>Zapisz daty meldunku</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Główne przyciski ──────────────────────────────── -->
|
||||
<transition name="fade">
|
||||
<div v-if="allFeedback" :class="['feedback', 'feedback-main', allFeedback.type]">
|
||||
{{ allFeedback.msg }}
|
||||
</div>
|
||||
</transition>
|
||||
</main>
|
||||
|
||||
<div class="bottom-bar">
|
||||
<div class="bottom-bar-inner">
|
||||
<button class="btn-ghost btn-lg" @click="$emit('back')">
|
||||
← Cofnij
|
||||
</button>
|
||||
<button
|
||||
class="btn-primary btn-lg"
|
||||
:disabled="saveAllLoading"
|
||||
@click="openModeDialog"
|
||||
>
|
||||
<span v-if="saveAllLoading" class="spinner spinner-dark" />
|
||||
<span v-else>Zapisz wszystko</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Modal: tryb zapisu ──────────────────────────────── -->
|
||||
<transition name="modal">
|
||||
<div v-if="showModeDialog" class="modal-backdrop" @click.self="showModeDialog = false">
|
||||
<div class="modal-box">
|
||||
<p class="modal-question">Rodzaj operacji dla apartamentu {{ nr }}?</p>
|
||||
<div class="modal-choices">
|
||||
<button class="modal-choice" @click="confirmMode('new')">
|
||||
<span class="choice-icon">○</span>
|
||||
<span class="choice-label">Nowy gość</span>
|
||||
<span class="choice-desc">Zapisz dane i aktywuj automatyzację świateł przy pierwszym wejściu</span>
|
||||
</button>
|
||||
<button class="modal-choice" @click="confirmMode('reset')">
|
||||
<span class="choice-icon">■</span>
|
||||
<span class="choice-label">Reset PIN / karty</span>
|
||||
<span class="choice-desc">Tylko aktualizacja danych dostępu, bez zmiany automatyzacji</span>
|
||||
</button>
|
||||
</div>
|
||||
<button class="modal-cancel" @click="showModeDialog = false">Anuluj</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { setPin, setCard, setCheckin, setCheckout, verifyAll, getLastChanged, setNewGuest } from '../services/ha.js'
|
||||
|
||||
const props = defineProps({
|
||||
nr: { type: Number, required: true },
|
||||
token: { type: String, required: true },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['back', 'logout'])
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────
|
||||
function pad(n) { return String(n).padStart(2, '0') }
|
||||
|
||||
function toDatetimeLocal(date) {
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`
|
||||
}
|
||||
|
||||
function defaultCheckin() {
|
||||
return toDatetimeLocal(new Date())
|
||||
}
|
||||
|
||||
function defaultCheckout() {
|
||||
const d = new Date()
|
||||
d.setDate(d.getDate() + 1)
|
||||
d.setHours(11, 0, 0, 0)
|
||||
return toDatetimeLocal(d)
|
||||
}
|
||||
|
||||
const FMT = new Intl.DateTimeFormat('pl-PL', {
|
||||
day: '2-digit', month: 'short', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit',
|
||||
})
|
||||
|
||||
function formatDate(date) {
|
||||
if (!date) return '—'
|
||||
return FMT.format(date)
|
||||
}
|
||||
|
||||
function showFeedback(target, type, msg, duration = 3500) {
|
||||
target.value = { type, msg }
|
||||
setTimeout(() => { target.value = null }, duration)
|
||||
}
|
||||
|
||||
// ── State ──────────────────────────────────────────────────
|
||||
const checkin = ref(defaultCheckin())
|
||||
const checkout = ref(defaultCheckout())
|
||||
const datesLoading = ref(false)
|
||||
const datesFeedback = ref(null)
|
||||
|
||||
const pin = ref('')
|
||||
const pinLoading = ref(false)
|
||||
const pinFeedback = ref(null)
|
||||
|
||||
const card = ref('')
|
||||
const cardLoading = ref(false)
|
||||
const cardFeedback = ref(null)
|
||||
|
||||
const updateDates = ref(true)
|
||||
|
||||
const saveAllLoading = ref(false)
|
||||
const allFeedback = ref(null)
|
||||
const showModeDialog = ref(false)
|
||||
|
||||
const pinLastChanged = ref(null)
|
||||
const cardLastChanged = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
const [pinDate, cardDate] = await Promise.all([
|
||||
getLastChanged(props.token, `input_number.apartament_${props.nr}`),
|
||||
getLastChanged(props.token, `input_text.apartament_karta_${props.nr}`),
|
||||
])
|
||||
pinLastChanged.value = pinDate
|
||||
cardLastChanged.value = cardDate
|
||||
})
|
||||
|
||||
// ── Dialog ─────────────────────────────────────────────────
|
||||
function openModeDialog() {
|
||||
showModeDialog.value = true
|
||||
}
|
||||
|
||||
async function confirmMode(mode) {
|
||||
showModeDialog.value = false
|
||||
await submitAll(mode)
|
||||
}
|
||||
|
||||
// ── Actions ────────────────────────────────────────────────
|
||||
async function submitAll(mode) {
|
||||
saveAllLoading.value = true
|
||||
allFeedback.value = null
|
||||
|
||||
const pinToSave = pin.value || null
|
||||
const cardToSave = card.value || null
|
||||
|
||||
try {
|
||||
// 1. Zapis do HA
|
||||
const writes = []
|
||||
if (updateDates.value) {
|
||||
writes.push(setCheckin(props.token, props.nr, checkin.value))
|
||||
writes.push(setCheckout(props.token, props.nr, checkout.value))
|
||||
}
|
||||
if (pinToSave) writes.push(setPin(props.token, props.nr, pinToSave))
|
||||
if (cardToSave) writes.push(setCard(props.token, props.nr, cardToSave))
|
||||
if (mode === 'new') writes.push(setNewGuest(props.token, props.nr, true))
|
||||
await Promise.all(writes)
|
||||
|
||||
// 2. Krótka chwila — HA potrzebuje ~200ms na aktualizację stanu
|
||||
await new Promise(r => setTimeout(r, 300))
|
||||
|
||||
// 3. Weryfikacja — odczyt z HA i porównanie
|
||||
const errors = await verifyAll(props.token, props.nr, {
|
||||
checkin: updateDates.value ? checkin.value : null,
|
||||
checkout: updateDates.value ? checkout.value : null,
|
||||
pin: pinToSave,
|
||||
card: cardToSave,
|
||||
})
|
||||
|
||||
if (errors.length === 0) {
|
||||
const now = new Date()
|
||||
if (pinToSave) pinLastChanged.value = now
|
||||
if (cardToSave) cardLastChanged.value = now
|
||||
pin.value = ''
|
||||
card.value = ''
|
||||
const modeMsg = mode === 'new' ? ' Flaga nowego gościa aktywowana.' : ''
|
||||
showFeedback(allFeedback, 'success', `Zapisano i zweryfikowano — wszystkie dane są poprawne.${modeMsg}`)
|
||||
} else {
|
||||
showFeedback(
|
||||
allFeedback,
|
||||
'error',
|
||||
'Zapis częściowo nieudany:\n' + errors.join('\n'),
|
||||
6000
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
showFeedback(allFeedback, 'error', `Błąd: ${err.message}`)
|
||||
} finally {
|
||||
saveAllLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitDates() {
|
||||
datesLoading.value = true
|
||||
try {
|
||||
await Promise.all([
|
||||
setCheckin(props.token, props.nr, checkin.value),
|
||||
setCheckout(props.token, props.nr, checkout.value),
|
||||
])
|
||||
showFeedback(datesFeedback, 'success', 'Daty meldunku zostały zapisane.')
|
||||
} catch (err) {
|
||||
showFeedback(datesFeedback, 'error', `Błąd: ${err.message}`)
|
||||
} finally {
|
||||
datesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitPin() {
|
||||
if (!pin.value || pinLoading.value) return
|
||||
pinLoading.value = true
|
||||
try {
|
||||
await setPin(props.token, props.nr, pin.value)
|
||||
pin.value = ''
|
||||
pinLastChanged.value = new Date()
|
||||
showFeedback(pinFeedback, 'success', 'Kod PIN został zaktualizowany.')
|
||||
} catch (err) {
|
||||
showFeedback(pinFeedback, 'error', `Błąd: ${err.message}`)
|
||||
} finally {
|
||||
pinLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitCard() {
|
||||
if (!card.value || cardLoading.value) return
|
||||
cardLoading.value = true
|
||||
try {
|
||||
await setCard(props.token, props.nr, card.value)
|
||||
card.value = ''
|
||||
cardLastChanged.value = new Date()
|
||||
showFeedback(cardFeedback, 'success', 'Numer karty został zaktualizowany.')
|
||||
} catch (err) {
|
||||
showFeedback(cardFeedback, 'error', `Błąd: ${err.message}`)
|
||||
} finally {
|
||||
cardLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 80%, rgba(185, 150, 60, 0.03) 0%, transparent 55%),
|
||||
var(--bg);
|
||||
}
|
||||
|
||||
/* ── Topbar ───────────────────────────────────────────────── */
|
||||
.topbar {
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: rgba(12, 12, 12, 0.92);
|
||||
backdrop-filter: blur(12px);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.topbar-inner {
|
||||
max-width: 680px;
|
||||
margin: 0 auto;
|
||||
padding: 0 32px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.brand { display: flex; align-items: center; gap: 10px; }
|
||||
.brand-mark { color: var(--gold); font-size: 9px; }
|
||||
.brand-name { font-family: var(--font-serif); font-size: 17px; letter-spacing: 0.03em; }
|
||||
.logout-btn { font-size: 12px; padding: 7px 16px; }
|
||||
|
||||
/* ── Content ─────────────────────────────────────────────── */
|
||||
.content {
|
||||
max-width: 680px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
padding: 36px 32px 60px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 32px;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
transition: color var(--transition);
|
||||
}
|
||||
|
||||
.back-btn:hover { color: var(--text); }
|
||||
.back-arrow { font-size: 16px; }
|
||||
|
||||
.apt-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.apt-badge {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border: 1px solid var(--gold);
|
||||
color: var(--gold);
|
||||
font-family: var(--font-serif);
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.apt-heading h2 {
|
||||
font-size: 26px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ── Checkbox ────────────────────────────────────────────── */
|
||||
.card-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.card-title-row .card-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.checkbox-custom {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: border-color var(--transition), background var(--transition);
|
||||
}
|
||||
|
||||
.checkbox-label input:checked + .checkbox-custom {
|
||||
background: var(--gold);
|
||||
border-color: var(--gold);
|
||||
}
|
||||
|
||||
.checkbox-label input:checked + .checkbox-custom::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 4px;
|
||||
height: 7px;
|
||||
border: 2px solid #000;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
transform: rotate(45deg) translate(-1px, -1px);
|
||||
}
|
||||
|
||||
.checkbox-label:hover .checkbox-custom {
|
||||
border-color: var(--gold);
|
||||
}
|
||||
|
||||
.dates-disabled {
|
||||
opacity: 0.35;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ── Cards ───────────────────────────────────────────────── */
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 28px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 22px;
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* ── Dates ───────────────────────────────────────────────── */
|
||||
.dates-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.field { margin-bottom: 4px; }
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.label-dot { font-size: 7px; }
|
||||
|
||||
/* Style datetime-local jak zwykły input */
|
||||
input[type="datetime-local"] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.card-actions .btn-primary {
|
||||
padding: 11px 24px;
|
||||
}
|
||||
|
||||
/* ── Access fields ───────────────────────────────────────── */
|
||||
.input-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.input-row input { flex: 1; }
|
||||
|
||||
.action-btn {
|
||||
padding: 10px 16px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Spinner ─────────────────────────────────────────────── */
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid rgba(0,0,0,0.3);
|
||||
border-top-color: #000;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.last-changed {
|
||||
margin-top: 7px;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.25s ease, transform 0.25s ease; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; transform: translateY(-4px); }
|
||||
|
||||
/* ── Bottom bar ──────────────────────────────────────────── */
|
||||
.bottom-bar {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
border-top: 1px solid var(--border);
|
||||
background: rgba(12, 12, 12, 0.95);
|
||||
backdrop-filter: blur(12px);
|
||||
z-index: 100;
|
||||
padding: 16px 32px;
|
||||
}
|
||||
|
||||
.bottom-bar-inner {
|
||||
max-width: 680px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
flex: 1;
|
||||
padding: 15px 24px;
|
||||
font-size: 14px;
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.spinner-dark {
|
||||
border-color: rgba(0,0,0,0.25);
|
||||
border-top-color: #000;
|
||||
}
|
||||
|
||||
.feedback-main {
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
/* ── Modal ───────────────────────────────────────────────── */
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
z-index: 200;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.modal-box {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 32px 28px 24px;
|
||||
width: 100%;
|
||||
max-width: 460px;
|
||||
}
|
||||
|
||||
.modal-question {
|
||||
font-family: var(--font-serif);
|
||||
font-size: 19px;
|
||||
color: var(--text);
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-choices {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.modal-choice {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 16px 20px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
display: grid;
|
||||
grid-template-columns: 20px 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
column-gap: 12px;
|
||||
row-gap: 4px;
|
||||
transition: background var(--transition), border-color var(--transition);
|
||||
}
|
||||
|
||||
.modal-choice:hover {
|
||||
background: rgba(185, 150, 60, 0.08);
|
||||
border-color: rgba(185, 150, 60, 0.4);
|
||||
}
|
||||
|
||||
.choice-icon {
|
||||
grid-row: 1 / 3;
|
||||
align-self: center;
|
||||
color: var(--gold);
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.choice-label {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 14px;
|
||||
color: var(--text);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.choice-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.modal-cancel {
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 13px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
transition: color var(--transition);
|
||||
}
|
||||
|
||||
.modal-cancel:hover { color: var(--text); }
|
||||
|
||||
/* Modal transition */
|
||||
.modal-enter-active, .modal-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.modal-enter-active .modal-box, .modal-leave-active .modal-box {
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
.modal-enter-from, .modal-leave-to { opacity: 0; }
|
||||
.modal-enter-from .modal-box, .modal-leave-to .modal-box {
|
||||
transform: translateY(12px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.topbar-inner, .content { padding-left: 16px; padding-right: 16px; }
|
||||
.dates-grid { grid-template-columns: 1fr; }
|
||||
.bottom-bar { padding: 12px 16px; }
|
||||
}
|
||||
</style>
|
||||
306
panel-dostepu/src/views/DashboardView.vue
Normal file
306
panel-dostepu/src/views/DashboardView.vue
Normal file
@@ -0,0 +1,306 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<header class="topbar">
|
||||
<div class="topbar-inner">
|
||||
<div class="brand">
|
||||
<span class="brand-mark">■</span>
|
||||
<span class="brand-name">Panel Dostępu</span>
|
||||
</div>
|
||||
<button class="btn-ghost logout-btn" @click="$emit('logout')">Wyloguj</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="content">
|
||||
<div class="page-title">
|
||||
<h2>Wybierz apartament</h2>
|
||||
<p class="page-sub">Kliknij apartament aby zarządzać dostępem i datami meldunku</p>
|
||||
</div>
|
||||
|
||||
<div class="apt-grid">
|
||||
<button
|
||||
v-for="nr in apartments"
|
||||
:key="nr"
|
||||
class="apt-tile"
|
||||
:disabled="resetting"
|
||||
@click="$emit('select', nr)"
|
||||
>
|
||||
<span class="apt-num">{{ nr }}</span>
|
||||
<span class="apt-label">Apartament</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="reset-section">
|
||||
<div class="divider" />
|
||||
|
||||
<!-- Potwierdzenie -->
|
||||
<div v-if="confirming" class="confirm-box">
|
||||
<p class="confirm-text">
|
||||
Wszystkie apartamenty (1–{{ apartments.length }}) zostaną zresetowane:<br />
|
||||
<strong>PIN → 2137</strong> | <strong>Karta → 0</strong>
|
||||
</p>
|
||||
<div class="confirm-actions">
|
||||
<button class="btn-ghost btn-sm" :disabled="resetting" @click="confirming = false">
|
||||
Anuluj
|
||||
</button>
|
||||
<button class="btn-danger btn-sm" :disabled="resetting" @click="doReset">
|
||||
<span v-if="resetting" class="spinner spinner-light" />
|
||||
<span v-else>Tak, resetuj wszystko</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="reset-row">
|
||||
<transition name="fade">
|
||||
<div v-if="resetFeedback" :class="['feedback', resetFeedback.type]">
|
||||
{{ resetFeedback.msg }}
|
||||
</div>
|
||||
</transition>
|
||||
<button class="btn-reset" @click="confirming = true">
|
||||
Resetuj wszystkie apartamenty
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<span>Panel Dostępu — tylko do użytku wewnętrznego</span>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { setPin, setCard } from '../services/ha.js'
|
||||
|
||||
const props = defineProps({
|
||||
token: { type: String, required: true },
|
||||
})
|
||||
|
||||
defineEmits(['logout', 'select'])
|
||||
|
||||
const apartments = Array.from(
|
||||
{ length: Number(import.meta.env.VITE_APARTMENTS) || 7 },
|
||||
(_, i) => i + 1
|
||||
)
|
||||
|
||||
const confirming = ref(false)
|
||||
const resetting = ref(false)
|
||||
const resetFeedback = ref(null)
|
||||
|
||||
async function doReset() {
|
||||
resetting.value = true
|
||||
resetFeedback.value = null
|
||||
try {
|
||||
await Promise.all(
|
||||
apartments.flatMap(nr => [
|
||||
setPin(props.token, nr, 2137),
|
||||
setCard(props.token, nr, '0'),
|
||||
])
|
||||
)
|
||||
resetFeedback.value = { type: 'success', msg: `Zresetowano ${apartments.length} apartamentów — PIN: 2137, Karta: 0` }
|
||||
setTimeout(() => { resetFeedback.value = null }, 5000)
|
||||
} catch (err) {
|
||||
resetFeedback.value = { type: 'error', msg: `Błąd resetu: ${err.message}` }
|
||||
} finally {
|
||||
resetting.value = false
|
||||
confirming.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background:
|
||||
radial-gradient(ellipse at 80% 10%, rgba(185, 150, 60, 0.03) 0%, transparent 55%),
|
||||
var(--bg);
|
||||
}
|
||||
|
||||
.topbar {
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: rgba(12, 12, 12, 0.92);
|
||||
backdrop-filter: blur(12px);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.topbar-inner {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 0 32px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.brand { display: flex; align-items: center; gap: 10px; }
|
||||
.brand-mark { color: var(--gold); font-size: 9px; }
|
||||
.brand-name { font-family: var(--font-serif); font-size: 17px; letter-spacing: 0.03em; }
|
||||
.logout-btn { font-size: 12px; padding: 7px 16px; }
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
padding: 48px 32px;
|
||||
}
|
||||
|
||||
.page-title { margin-bottom: 40px; }
|
||||
.page-title h2 { font-size: 30px; margin-bottom: 8px; }
|
||||
.page-sub { color: var(--text-muted); font-size: 13px; }
|
||||
|
||||
/* ── Apartment tiles ─────────────────────────────────────── */
|
||||
.apt-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.apt-tile {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 32px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
transition: border-color var(--transition), background var(--transition), transform var(--transition);
|
||||
font-family: inherit;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.apt-tile:hover:not(:disabled) {
|
||||
border-color: var(--gold);
|
||||
background: #1e1c17;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.apt-tile:active:not(:disabled) { transform: translateY(0); }
|
||||
.apt-tile:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
|
||||
.apt-num {
|
||||
font-family: var(--font-serif);
|
||||
font-size: 42px;
|
||||
color: var(--gold);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.apt-label {
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ── Reset section ───────────────────────────────────────── */
|
||||
.reset-section { margin-top: 40px; }
|
||||
|
||||
.reset-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn-reset {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(139, 58, 58, 0.4);
|
||||
color: #c97a7a;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.07em;
|
||||
text-transform: uppercase;
|
||||
padding: 9px 18px;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
transition: border-color var(--transition), color var(--transition), background var(--transition);
|
||||
}
|
||||
|
||||
.btn-reset:hover {
|
||||
border-color: rgba(139, 58, 58, 0.8);
|
||||
color: #e08080;
|
||||
background: rgba(139, 58, 58, 0.07);
|
||||
}
|
||||
|
||||
/* Confirm box */
|
||||
.confirm-box {
|
||||
background: rgba(139, 58, 58, 0.08);
|
||||
border: 1px solid rgba(139, 58, 58, 0.3);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 20px 24px;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.confirm-text {
|
||||
font-size: 14px;
|
||||
color: var(--text);
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.confirm-text strong { color: #e08080; }
|
||||
|
||||
.confirm-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-sm { padding: 8px 18px; font-size: 12px; }
|
||||
|
||||
.btn-danger {
|
||||
background: #7a2a2a;
|
||||
color: #f0d0d0;
|
||||
border: none;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.07em;
|
||||
text-transform: uppercase;
|
||||
padding: 8px 18px;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: background var(--transition);
|
||||
}
|
||||
|
||||
.btn-danger:hover:not(:disabled) { background: #8f3232; }
|
||||
.btn-danger:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid rgba(255,255,255,0.3);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.25s; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
|
||||
.footer {
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 16px 32px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.topbar-inner, .content { padding-left: 16px; padding-right: 16px; }
|
||||
.apt-grid { grid-template-columns: repeat(3, 1fr); }
|
||||
}
|
||||
</style>
|
||||
137
panel-dostepu/src/views/LoginView.vue
Normal file
137
panel-dostepu/src/views/LoginView.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<div class="login-wrap">
|
||||
<div class="login-box">
|
||||
<div class="logo">
|
||||
<div class="logo-mark">■</div>
|
||||
<h1>Panel Dostępu</h1>
|
||||
<p class="subtitle">System zarządzania dostępem</p>
|
||||
</div>
|
||||
|
||||
<div class="divider" />
|
||||
|
||||
<form @submit.prevent="handleLogin">
|
||||
<div class="field">
|
||||
<label for="username">Login</label>
|
||||
<input
|
||||
id="username"
|
||||
v-model="username"
|
||||
type="text"
|
||||
placeholder="Login"
|
||||
autocomplete="username"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="password">Hasło</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
placeholder="Hasło"
|
||||
autocomplete="current-password"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="feedback error">{{ error }}</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn-primary login-btn"
|
||||
:disabled="!username || !password || loading"
|
||||
>
|
||||
<span v-if="loading" class="spinner spinner-dark" />
|
||||
<span v-else>Zaloguj się</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { panelLogin } from '../services/auth.js'
|
||||
|
||||
const emit = defineEmits(['authenticated'])
|
||||
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
async function handleLogin() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
// Krótkie opóźnienie żeby przycisk nie migał przy szybkim kliknięciu
|
||||
await new Promise(r => setTimeout(r, 300))
|
||||
if (panelLogin(username.value, password.value)) {
|
||||
emit('authenticated')
|
||||
} else {
|
||||
error.value = 'Nieprawidłowy login lub hasło.'
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-wrap {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 50%, rgba(185, 150, 60, 0.04) 0%, transparent 60%),
|
||||
var(--bg);
|
||||
}
|
||||
|
||||
.login-box {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 40px 36px;
|
||||
}
|
||||
|
||||
.logo { text-align: center; margin-bottom: 28px; }
|
||||
.logo-mark { color: var(--gold); font-size: 10px; letter-spacing: 6px; margin-bottom: 14px; opacity: 0.8; }
|
||||
.logo h1 { font-size: 26px; color: var(--text); margin-bottom: 6px; }
|
||||
.subtitle { color: var(--text-muted); font-size: 13px; letter-spacing: 0.04em; }
|
||||
|
||||
.field { margin-bottom: 14px; }
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
padding: 13px;
|
||||
font-size: 13px;
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.spinner-dark {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid rgba(0,0,0,0.25);
|
||||
border-top-color: #000;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
</style>
|
||||
28
panel-dostepu/vite.config.js
Normal file
28
panel-dostepu/vite.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
const haUrl = env.VITE_HA_URL || 'http://10.20.10.106:8123'
|
||||
|
||||
return {
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 5173,
|
||||
host: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: haUrl,
|
||||
changeOrigin: true,
|
||||
configure: (proxy) => {
|
||||
proxy.on('proxyReq', (proxyReq, req) => {
|
||||
// Upewnij się że Authorization header jest przekazywany
|
||||
const auth = req.headers['authorization']
|
||||
if (auth) proxyReq.setHeader('Authorization', auth)
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user