Guest Access Website (Secure?) on LAN

I’ve wanted to years to be able to let a guest/employee access some resources in our home automation system. I finally figured out one solution.

A big thanks to @tmjpugh who helped me out over on this thread. All of the original structure / idea is here because of their work.

There are a few features:

  • Guest website available when connected to local Wi-Fi - You could use a QR code to let them connect to your Guest Wi-Fi, and to link to the URL.
  • Logs who the last person to successfully enter a pin / OTP.
  • Logs the last person to request access via the “doorbell”.
  • Prompts a phone call via an actionable notification (may need tweaking for iOS).

Screenshots



Things I’d love to make better, but don’t know how. Can you help me?

There are a few things I’d like to be better:

  • Easier process to add/remove users (something that would pass HAF)
    • Currently the process requires adding a OTP sensor or input_number and modifying the automation.
  • Currently there is an input_number.set_value to NA then a delay of 1 second so that a value is logged if it’s the same as the last entry.
    • It would be great to know if there’s a better way to log repetitive entries of the same value.
  • Include a photo from a security camera in the notification.

SECURITY NOTE / REQUEST

The webhook string is visible to anyone visiting the website and inspecting the headers. I don’t believe this is a security issues since they don’t trigger anything critical without a condition that checks for a pin or OTP. However, anyone with a strength in security, please take a look to see if you see any weaknesses.

Requirements

We have a few things going here to make this work:

  • nginx for webserver and reverse proxy
  • Local DNS, eg. PiHole
  • HA - automation
  • HA - helpers

Lets start with getting a website up:

Copy and paste the following index.html into a text editor. You can find and replace the following:

  • your home
  • your address
  • your_GUEST_HA_domain.com

You may also want to place a logo.png or similar for the background in the same directory as the index.html file.

index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Welcome to your home</title>

    <style>
      body {
        color: #000000;
        /*text-shadow: 10px 10px 10px #000000;*/
        background-image: url("logo.png");
        width: 50%;
        min-height: 100vh;
        background-repeat: no-repeat;
        background-position: 55% center;
        background-attachment: fixed;
        -webkit-background-size: 50%;
        -moz-background-size: 50%;
        -o-background-size: 50%;
        background-size: 50%;
      }

      .btn {
        background: #3498db;
        background-image: -webkit-linear-gradient(top, #3498db, #2980b9);
        background-image: -moz-linear-gradient(top, #3498db, #2980b9);
        background-image: -ms-linear-gradient(top, #3498db, #2980b9);
        background-image: -o-linear-gradient(top, #3498db, #2980b9);
        background-image: linear-gradient(to bottom, #3498db, #2980b9);
        -webkit-border-radius: 33;
        -moz-border-radius: 33;
        border-radius: 33px;
        -webkit-box-shadow: 4px 5px 3px #697cdb;
        -moz-box-shadow: 4px 5px 3px #697cdb;
        box-shadow: 4px 5px 3px #697cdb;
        font-family: Arial;
        color: #f5ebf5;
        font-size: 20px;
        padding: 10px 20px 10px 20px;
        text-decoration: none;
        margin: 12px;
      }

      .btn:hover {
        background: #29dde3;
        background-image: -webkit-linear-gradient(top, #29dde3, #3498db);
        background-image: -moz-linear-gradient(top, #29dde3, #3498db);
        background-image: -ms-linear-gradient(top, #29dde3, #3498db);
        background-image: -o-linear-gradient(top, #29dde3, #3498db);
        background-image: linear-gradient(to bottom, #29dde3, #3498db);
        text-decoration: none;
      }
    </style>
  </head>
  <body>
    <input type="text" id="currentDateTime" />
    <br />
    <h1>your home</h1>
    <h2>your address</h2>

    <br />

    <button class="btn" onclick="Doorbell()">Ring Doorbell</button>
    <button class="btn" onclick="OpenGate()">Open Garage</button>

    <script>
      function Doorbell() {
        let phonenum = prompt("Please Enter Your Phone Number", "");
        var request = new XMLHttpRequest();
        request.open("POST", "https://your_GUEST_HA_domain.com/doorbell");

        request.setRequestHeader("Content-type", "application/json");

        request.send(JSON.stringify(phonenum));
      }
    </script>

    <script>
      function OpenGate() {
        let code = prompt("Enter Code", "");

        if (code != null) {
          var request = new XMLHttpRequest();
          request.open("POST", "https://your_GUEST_HA_domain.com/garage");

          request.setRequestHeader("Content-type", "application/json");

          request.send(JSON.stringify(code));
        }
      }
    </script>

    <script>
      var today = new Date();
      var date = today.getFullYear() + "-" + (today.getMonth() + 1) + "-" + today.getDate();
      var time = today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds();
      var dateTime = date + " " + time;
      document.getElementById("currentDateTime").value = dateTime;
    </script>
  </body>
</html>

That gets saved in your /data/guest_server_npm directory within nginx, or wherever you are hosting website that nginx can serve from.

OTP Integration

For each person that you want to have a unique OTP, create a OTP token and give it to them.

Alternatively you could use an input_number helper to store a pin.

Helpers

You’ll need a couple of helpers to log everything:
input_text.last_garage_code_or_person
input_text.last_doorbell_phone_number

Automation

The following need to be changed. If you create new webhooks via the GUI it will generate a unique webhook ID. You would have to click the trigger menu and assign the respective Trigger ID.

  • -your_doorbell_webhook
  • -your_garage_webhook

Find and replace:

  • sensor.person_1_otp_sensor with the OTP sensor OR input_number that stores the pin
  • Person 1 with the name of Person 1
    Repeat that for each person, and delete or add as needed.

If you wanted additional conditions you could add them for each user and use an and: eg:

  • time based access (use a scheduler to turn an input_boolean on and off that is checked)

If you’re using iOS, you may need to update the notification so that your Dial Number intent works.

Automation YAML
alias: Guest Website
description: ""
triggers:
  - allowed_methods:
      - POST
      - PUT
    local_only: true
    webhook_id: "-your_garage_webhook"
    id: garage
    trigger: webhook
  - allowed_methods:
      - POST
      - PUT
    local_only: true
    webhook_id: "-your_doorbell_webhook"
    id: doorbell
    trigger: webhook
conditions: []
actions:
    if:
      - condition: trigger
        id:
          - code
      - condition: or
        conditions:
          - condition: template
            value_template: "{{ (trigger.json) == (states.sensor.person_1_otp_sensor.state) }}"
          - condition: template
            value_template: "{{ (trigger.json) == (states.sensor.person_2_otp_sensor.state) }}"
          - condition: template
            value_template: "{{ (trigger.json) == (states.sensor.person_3_otp_sensor.state) }}"
    then:
      - action: notify.mobile_app_yours
        data:
          message: >-
            The Garage Door was Activated By {% if (trigger.json) ==
            (states.sensor.person_1_otp_sensor.state) %}   Person 1 {% elif
            (trigger.json) == (states.sensor.person_2_otp_sensor.state) %}   Person 2
            {% elif (trigger.json) == (states.sensor.person_3_otp_sensor.state)
            %}   Person 3 {% else %}   Error {% endif %}
          data:
            importance: high
            color: red
            tag: garage_doors
            channel: house_quiet_alerts
            visibility: public
            notification_icon: mdi:garage-alert
          title: Garage Door Alert
      - action: input_text.set_value
        metadata: {}
        data:
          value: NA
        target:
          entity_id: input_text.last_garage_code_or_person
      - delay:
          hours: 0
          minutes: 0
          seconds: 1
          milliseconds: 0
      - action: input_text.set_value
        metadata: {}
        data:
          value: >-
            Garage Activated By{% if (trigger.json) ==
            (states.sensor.person_1_otp_sensor.state) %} Person 1 {% elif
            (trigger.json) == (states.sensor.Person_2_otp_sensor.state) %} Person 2 {%
            elif (trigger.json) == (states.sensor.person_3_otp_sensor.state) %}
            Person 3 {% else %} NA {% endif %}
        target:
          entity_id: input_text.last_garage_code_or_person
  - if:
      - condition: trigger
        id:
          - doorbell
    then:
      - action: notify.mobile_app_yours
        data:
          message: Access Requested by {{trigger.json}}
          data:
            importance: high
            color: yellow
            tag: garage_doors
            channel: house_quiet_alerts
            visibility: public
            notification_icon: mdi:doorbell
            actions:
              - action: URI
                title: Call Back {{trigger.json}}
                uri: >-
                  intent://#Intent;scheme=tel:{{trigger.json}};action=android.intent.action.DIAL;end
          title: Doorbell!
      - action: input_text.set_value
        metadata: {}
        data:
          value: NA
        target:
          entity_id: input_text.last_doorbell_phone_number
      - delay:
          hours: 0
          minutes: 0
          seconds: 1
          milliseconds: 0
      - action: input_text.set_value
        metadata: {}
        data:
          value: "{{trigger.json}}"
        target:
          entity_id: input_text.last_doorbell_phone_number
mode: single

Nginx

I’m using nginx proxy manager. Create a new proxy host with the following:

  • Domain Name = your_GUEST_HA_domain.com
  • Scheme = http
  • Forward Hostname / IP = 127.0.0.1
  • Forward Port = 80

In the Advanced tab place the following snippit with changes to:

  • -your_doorbell_webhook
  • -your_garage_webhook
  • your_HA_domain.com
nginx configuration
location / {
	root /data/guest_server_npm;
}

location /doorbell {
	return 308 https://your_HA_domain.com/api/webhook/-your_doorbell_webhook;
}

location /garage {
	return 308 https://your_HA_domain.com/api/webhook/-your_garage_webhook;
}

Dashboard Cards

You can add this to your dashboard if you want a log of who activated the gate/garage door.

Dashboard Card
type: custom:logbook-card
entity: input_text.last_garage_code_or_person
hidden_state:
  - Exit*
show:
  state: true
  duration: false
  start_date: true
  end_date: false
  icon: false
  separator: true
  entity_name: true
title: Garage Pin History
max_items: 10
collapse: 2
custom_logs: false
view_layout:
  position: main

I think that’s everything, please let me know if something is missing or doesn’t make sense.

Again, this is all made possible by @tmjpugh who got me started. Thank you!

I’m very interested to hear any tips that could make this better, and certainly if there are any security concerns.