Bläddra i källkod

2026-05-29 08:53:10: Toward "RoomState" v0.6

ZiB 5 månader sedan
förälder
incheckning
9b5172afb0

+ 14 - 20
README.md

@@ -1,8 +1,13 @@
 # zib_guest
 
-## Guest reservation integration for Home Assistant
+## Guest a guest room management integration for Home Assistant
 
-##### This integration provides any number of devices with entities for:
+#### Room State
+
+The `State` device to tracks the current room state — whether or not there is an active reservation — and provides configuration options to control automatic state switching and default reseveration data
+
+#### Reservations
+The devices `Current` and `Next` track reservation data with the following entities:
 
 - Guest name
 - Door code
@@ -22,26 +27,15 @@
 - Departure
 	- [Same attributes as Arrival]
 - Check in
-	- [Used in Arrival time attributes]
+	- [Used in Arrival time components]
 - Check out
-	- [Used in Departure time attributes]
+	- [Used in Departure time components]
 - WiFi SSID
 - WiFi key
 	- qr `'WIFI:S:' + <wifi_ssid> + ';T:WPA;P:' + <wifi_key> + ';;'`
 
-I use this data to feed both guest-facing displays, where guests can see WiFi and other information about their own
-reservation, and for backend systems that tell me about upcoming reservations or control automations in relation to
-the reservation state
-
-All entites are settable via a service call. I use an external program connected via the WS API to update these sensors
-based on my rental calendar and other external data. There are many ways this data could come into HA, but I wanted to
-use the WS API specifically for a number of reasons: it has an active connection so I'll know right away when HA
-disconnects, it uses simple tokens for auth, clients for several programming environments already exist, I can publish
-from anywhere that can reach the frontend HTTP instance
-
-This project also covers at least the minimal parts required for any HA integration to publish async push sensors, add
-extra attributes, use the config_flow UI, and receive service calls. Feel free to use it as an example if you are working
-on your own integration. The HA team also [publishes 
-examples](https://github.com/home-assistant/example-custom-config/tree/master) of many parts of the integration framework. 
-If you want all the modern features start with the [detailed_hello_world_push 
-example](https://github.com/home-assistant/example-custom-config/tree/master/custom_components/detailed_hello_world_push)
+I use this data to feed both guest-facing displays, where Aguests can see WiFi and other information about their own reservation, and for backend systems that tell me about upcoming reservations or control automations in relation to the reservation state
+
+All entites are settable via a service call. I use an external program connected via the WS API to update these sensors based on my rental calendar and other external data. There are many ways this data could come into HA, but I wanted to use the WS API specifically for a number of reasons: it has an active connection so I'll know right away when HA disconnects, it uses simple tokens for auth, clients for several programming environments already exist, I can publish from anywhere that can reach the frontend HTTP instance
+
+This project also covers at least the minimal parts required for any HA integration to publish async push sensors, add extra attributes, use the config_flow UI, and receive service calls. Feel free to use it as an example if you are working on your own integration. The HA team also [publishes examples](https://github.com/home-assistant/example-custom-config/tree/master) of many parts of the integration framework.  If you want all the modern features start with the [detailed_hello_world_push example](https://github.com/home-assistant/example-custom-config/tree/master/custom_components/detailed_hello_world_push)

+ 28 - 12
custom_components/zib_guest/__init__.py

@@ -6,12 +6,12 @@ from homeassistant.helpers.device import async_entity_id_to_device
 from homeassistant.helpers.device_registry import DATA_REGISTRY
 from homeassistant.helpers.entity_component import DATA_INSTANCES
 
-from .reservation import Reservation
+from .room import Room, Reservation, RoomState
 from .const import DOMAIN, PLATFORMS, RESERVATION_FIELDS
-type ZiBConfigEntry = ConfigEntry[Reservation]
+type ZiBConfigEntry = ConfigEntry[Room]
 
 async def async_setup_entry(hass: HomeAssistant, entry: ZiBConfigEntry) -> bool:
-  entry.runtime_data = Reservation(hass, entry.data["Name"])
+  entry.runtime_data = Room(hass, entry.data["Name"])
   await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
   return True
 
@@ -21,8 +21,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ZiBConfigEntry) -> bool
 
 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
   @callback
-  async def get_reservations(devices, entities) -> list:
-    reservations = []
+  async def get_devices(devices, entities, type = None) -> list:
+    matches = []
 
     if devices:
       for device in devices:
@@ -32,22 +32,27 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
         conf = hass.config_entries.async_get_entry(dev.primary_config_entry)
         if not conf:
           continue
-        reservations.append(conf.runtime_data)
+        matches.append(conf.runtime_data)
 
     if entities:
       for entity in hass.data[DATA_INSTANCES][Platform.SENSOR].entities:
         if entity.entity_id in entities:
-          reservations.append(entity.platform.config_entry.runtime_data)
+          matches.append(entity.platform.config_entry.runtime_data)
 
-    for res in reservations:
-      if not isinstance(res, Reservation):
-        reservations.remove(res);
+    if type is not None:
+      for match in matches:
+        if not isinstance(match, type):
+          matches.remove(match);
 
-    return reservations
+    return match
+  async def get_reservations(devices, entities) -> list:
+    return get_devices(devices, entities, Reservation)
+  async def get_room_state(devices, entities) -> list:
+    return get_devices(devices, entities, RoomState)
 
   @callback
   async def set_reservation(call: ServiceCall) -> None:
-    reservations = await get_reservations(call.data.get('device_id'), call.data.get('entity_id'))
+    reservations = await get_devices(call.data.get('device_id'), call.data.get('entity_id'), Reservation)
     for res in reservations:
       for field in RESERVATION_FIELDS:
         value = call.data.get(field)
@@ -56,4 +61,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
       await res.publish_updates()
   hass.services.async_register(DOMAIN, 'set_reservation', set_reservation)
 
+  @callback
+  async def set_roomstate(call: ServiceCall) -> None:
+    roomstates = await get_devices(call.data.get('device_id'), call.data.get('entity_id'), RoomState)
+    for room in roomstates:
+      for field in RESERVATION_FIELDS:
+        value = call.data.get(field)
+        if value is not None:
+          room.set(field, value)
+      await room.publish_updates()
+  hass.services.async_register(DOMAIN, 'set_roomstate', set_roomstate)
+
   return True

+ 8 - 9
custom_components/zib_guest/config_flow.py

@@ -3,28 +3,27 @@ import voluptuous as vol
 from homeassistant import config_entries, exceptions
 from homeassistant.core import HomeAssistant
 
-from .const import DOMAIN  # pylint:disable=unused-import
-from .reservation import Reservation
+from .const import DOMAIN
+from .room import Room
 
 DATA_SCHEMA = vol.Schema({("Name"): str})
 
-class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+class ConfigFlow(config_entries.ConfigFlow, domain = DOMAIN):
   VERSION = 1
   CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
 
-  async def async_step_user(self, user_input=None):
+  async def async_step_user(self, user_input = None):
     errors = {}
     if user_input is not None:
       try:
-        reservation = Reservation(self.hass, user_input['Name'])
         for entry in self._async_current_entries():
-          if entry.title == reservation.name:
+          if entry.title == user_input['Name']:
             raise DuplicateConfig
 
-        return self.async_create_entry(title=reservation.name, data=user_input)
+        return self.async_create_entry(title=user_input['Name'], data=user_input)
       except DuplicateConfig:
-        errors["base"] = "Duplicate reservation name"
-      except Exception:  # pylint: disable=broad-except
+        errors["base"] = "Duplicate room name"
+      except Exception:
         errors["base"] = "Unexpected exception"
 
     # Re-prompt if validation failed

+ 11 - 3
custom_components/zib_guest/const.py

@@ -1,11 +1,19 @@
 from homeassistant.const import Platform
 
-DOMAIN = "zib_guest"
-PLATFORMS = [Platform.SENSOR]
+DOMAIN = "zib_dev"
+PLATFORMS = [Platform.SENSOR, Platform.SWITCH, Platform.TEXT]
 
 RESERVATION_FIELDS = [
   'guest_name', 'door_code',
   'arrival', 'departure',
   'check_in', 'check_out',
   'wifi_ssid', 'wifi_key',
-]
+]
+
+ROOM_STATE_SWITCHES = [
+  'guest_mode', 'guest_mode_auto',
+]
+ROOM_STATE_TEXTS = [
+  'reservation_defaults',
+]
+ROOM_STATE_FIELDS = ROOM_STATE_SWITCHES + ROOM_STATE_TEXTS

+ 37 - 0
custom_components/zib_guest/entity.py

@@ -0,0 +1,37 @@
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import Entity
+
+from .const import DOMAIN
+
+class ZiBEntityBase(Entity):
+  should_poll = False
+  _attr_has_entity_name = True
+
+  def __init__(self, entity: Object, name: str, id: str = None, icon: str = None):
+    self._entity = entity
+    self._name = name
+    self._icon = icon
+    self._id = id if id is not None else name.lower().replace(' ', '_')
+    self._attr_unique_id = self._id
+    self._attr_name = name
+
+  async def async_added_to_hass(self):
+    self._entity.register_callback(self.async_write_ha_state)
+  async def async_will_remove_from_hass(self):
+    self._entity.remove_callback(self.async_write_ha_state)
+
+  @property
+  def device_info(self):
+    return {"identifiers": {(DOMAIN, self._entity.id)}}
+  @property
+  def available(self) -> bool:
+    return self._entity.online
+  @property
+  def id(self):
+    return self._id
+  @property
+  def name(self):
+    return self._name
+  @property
+  def icon(self):
+    return self._icon

+ 2 - 2
custom_components/zib_guest/manifest.json

@@ -1,6 +1,6 @@
 {
-  "domain": "zib_guest",
-  "name": "ZiB Guest",
+  "domain": "zib_dev",
+  "name": "ZiB Dev",
   "codeowners": [],
   "config_flow": true,
   "dependencies": [],

+ 0 - 75
custom_components/zib_guest/reservation.py

@@ -1,75 +0,0 @@
-from __future__ import annotations
-import asyncio, datetime
-from homeassistant.core import HomeAssistant
-
-from .const import RESERVATION_FIELDS
-
-class Reservation:
-  def __init__(self, hass: HomeAssistant, name: str) -> None:
-    self._hass = hass
-    self._loop = asyncio.get_event_loop()
-    self._callbacks = set()
-
-    self._id = name.lower().replace(' ', '_')
-    self._name = name
-    self._online = True
-
-    self._data = {}
-    for field in RESERVATION_FIELDS:
-      self._data[field] = None
-
-  def register_callback(self, callback: Callable[[], None]) -> None:
-    self._callbacks.add(callback)
-  def remove_callback(self, callback: Callable[[], None]) -> None:
-    self._callbacks.discard(callback)
-  async def publish_updates(self) -> None:
-    for callback in self._callbacks:
-      callback()
-
-  @property
-  def id(self) -> str:
-    return self._id
-  @property
-  def name(self) -> str:
-    return self._name
-  @property
-  def online(self) -> bool:
-    """ TODO: Expire or something """
-    return self._online
-
-  @property
-  def guest_name(self) -> str:
-    return self._data['guest_name']
-  @property
-  def door_code(self) -> str:
-    return self._data['door_code']
-
-  @property
-  def arrival(self) -> str:
-    return self._data['arrival'].isoformat()
-  @property
-  def departure(self) -> str:
-    return self._data['departure'].isoformat()
-
-  @property
-  def check_in(self) -> str:
-    return self._data['check_in'].isoformat()
-  @property
-  def check_out(self) -> str:
-    return self._data['check_out'].isoformat()
-
-  @property
-  def wifi_ssid(self) -> str:
-    return self._data['wifi_ssid']
-  @property
-  def wifi_key(self) -> str:
-    return self._data['wifi_key']
-
-  def set(self, field, value):
-    if field not in RESERVATION_FIELDS:
-      raise KeyError(f'Unknown field: {field}')
-    if field in ['arrival', 'departure']:
-      value = datetime.date.fromisoformat(value)
-    elif field in ['check_in', 'check_out']:
-      value = datetime.time.fromisoformat(value)
-    self._data[field] = value

+ 138 - 0
custom_components/zib_guest/room.py

@@ -0,0 +1,138 @@
+from __future__ import annotations
+import asyncio, datetime
+from homeassistant.core import HomeAssistant
+from homeassistant.const import STATE_ON, STATE_OFF
+
+from .const import RESERVATION_FIELDS, ROOM_STATE_FIELDS, ROOM_STATE_SWITCHES, ROOM_STATE_TEXTS
+
+class ZiBDeviceBase():
+  def __init__(self, hass: HomeAssistant, name: str, id: str = None) -> None:
+    self._hass = hass
+    self._loop = asyncio.get_event_loop()
+    self._callbacks = set()
+    self._name = name
+    self._id = id if id is not None else name.lower().replace(' ', '_')
+  @property
+  def id(self) -> str:
+    return self._id
+  @property
+  def name(self) -> str:
+    return self._name
+  @property
+  def online(self) -> bool:
+    return True
+
+  def register_callback(self, callback: Callable[[], None]) -> None:
+    self._callbacks.add(callback)
+  def remove_callback(self, callback: Callable[[], None]) -> None:
+    self._callbacks.discard(callback)
+  async def publish_updates(self) -> None:
+    for callback in self._callbacks:
+      callback()
+
+class Room(ZiBDeviceBase):
+  def __init__(self, hass: HomeAssistant, name: str) -> None:
+    super().__init__(hass, name)
+    self._data = {
+      'next':
+        Reservation(self._hass, f"{self._name} Next", self),
+      'current':
+        Reservation(self._hass, f"{self._name} Current", self),
+      'state':
+        RoomState(self._hass, f"{self._name} State", self),
+    }
+  @property
+  def state(self) -> RoomState:
+    return self._data['state']
+  @property
+  def next(self) -> Reservation:
+    return self._data['next']
+  @property
+  def current(self) -> Reservation:
+    return self._data['current']
+  @property
+  def reservations(self) -> list[Reservation]:
+    return [ self.next, self.current ]
+
+class Reservation(ZiBDeviceBase):
+  def __init__(self, hass: HomeAssistant, name: str, room: Room) -> None:
+    super().__init__(hass, name, f"{room.id}_{name.lower().replace(' ', '_')}")
+    self._room = room
+    self._online = True
+    self._data = {}
+    for field in RESERVATION_FIELDS:
+      self._data[field] = None
+  @property
+  def room(self) -> Room:
+    return self._room
+  @property
+  def online(self) -> bool:
+    """ TODO: Expire or something """
+    return self._online
+
+  @property
+  def guest_name(self) -> str:
+    return self._data['guest_name']
+  @property
+  def door_code(self) -> str:
+    return self._data['door_code']
+
+  @property
+  def arrival(self) -> str:
+    return self._data['arrival'].isoformat()
+  @property
+  def departure(self) -> str:
+    return self._data['departure'].isoformat()
+
+  @property
+  def check_in(self) -> str:
+    return self._data['check_in'].isoformat()
+  @property
+  def check_out(self) -> str:
+    return self._data['check_out'].isoformat()
+
+  @property
+  def wifi_ssid(self) -> str:
+    return self._data['wifi_ssid']
+  @property
+  def wifi_key(self) -> str:
+    return self._data['wifi_key']
+
+  def set(self, field, value):
+    if field not in RESERVATION_FIELDS:
+      raise KeyError(f'Unknown field: {field}')
+    if field in ['arrival', 'departure']:
+      value = datetime.date.fromisoformat(value)
+    elif field in ['check_in', 'check_out']:
+      value = datetime.time.fromisoformat(value)
+    self._data[field] = value
+
+class RoomState(ZiBDeviceBase):
+  def __init__(self, hass: HomeAssistant, name: str, room: Room) -> None:
+    super().__init__(hass, name, f"{room.id}_{name.lower().replace(' ', '_')}")
+    self._room = room
+    self._data = {}
+    for field in ROOM_STATE_FIELDS:
+      self._data[field] = None
+  @property
+  def room(self) -> Room:
+    return self._room
+
+  @property
+  def guest_mode(self) -> bool:
+      return bool(self._data['guest_mode'])
+  @property
+  def guest_mode_auto(self) -> bool:
+      return bool(self._data['guest_mode_auto'])
+
+  @property
+  def default_wifi_ssid(self) -> bool:
+      return self._data['default_wifi_ssid']
+
+  def set(self, field, value):
+    if field not in ROOM_STATE_FIELDS:
+      raise KeyError(f'Unknown switch: {field}')
+    if field in ROOM_STATE_SWITCHES:
+      self._data[field] = STATE_ON if bool(value) else STATE_OFF
+    else:
+      self._data[field] = value

+ 72 - 92
custom_components/zib_guest/sensor.py

@@ -1,141 +1,121 @@
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.entity import Entity
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.components.sensor import SensorDeviceClass
-import datetime
+from homeassistant.components.sensor import SensorEntity, SensorDeviceClass
+import datetime, re
 
 from . import ZiBConfigEntry
-from .const import DOMAIN
+from .entity import ZiBEntityBase
+from .room import Reservation
+from .const import DOMAIN, RESERVATION_FIELDS
 
 async def async_setup_entry(
   hass: HomeAssistant,
   config_entry: ZiBConfigEntry,
   async_add_entities: AddEntitiesCallback,
 ) -> None:
-  reservation = config_entry.runtime_data
-  entities = []
-  entities.append(Guest_Name(reservation))
-  entities.append(Door_Code(reservation))
-  entities.append(Arrival(reservation))
-  entities.append(Departure(reservation))
-  entities.append(Check_In(reservation))
-  entities.append(Check_Out(reservation))
-  entities.append(WiFi_SSID(reservation))
-  entities.append(WiFi_Key(reservation))
-  async_add_entities(entities)
-
-class ZiBBase(Entity):
-  should_poll = False
-  _attr_has_entity_name = True
-
-  def __init__(self, reservation: Reservation, name: str, icon: str = None):
-    self._reservation = reservation
-    self._name = name
-    self._icon = icon
-    self._id = name.lower().replace(' ', '_')
-    self._attr_unique_id = f"{self._reservation.id}_{self._id}"
-    self._attr_name = self._name
-
+  for reservation in config_entry.runtime_data.reservations:
+    async_add_entities([
+      Guest_Name(reservation),
+      Door_Code(reservation),
+      Arrival(reservation),
+      Departure(reservation),
+      Check_In(reservation),
+      Check_Out(reservation),
+      WiFi_SSID(reservation),
+      WiFi_Key(reservation),
+    ])
+
+def date_parts(dt: datetime) -> dict:
+  return {
+    'datetime': dt.isoformat(),
+    'datetime_abbr': dt.strftime('%Y-%m-%d @ %-I %p'),
+    'date': dt.strftime('%Y-%m-%d'),
+    'date_weekday': dt.strftime('%a, %b %-d'),
+    'datetime_weekday': dt.strftime('%a, %b %-d @ %-I %p'),
+    'time': dt.strftime('%H:%M:%S'),
+    'time_abbr': dt.strftime('%H:%M'),
+    'hour': re.sub('^0', ' ', dt.strftime('%I %p')),
+    'weekday': dt.strftime('%A'),
+    'weekday_abbr': dt.strftime('%a'),
+    'month': dt.strftime('%B'),
+    'month_abbr': dt.strftime('%b'),
+  }
+
+class ZiBSensorBase(ZiBEntityBase, SensorEntity):
+  def __init__(self, entity: Entity, name: str, icon: str = None):
+    self._field = name.lower().replace(' ', '_')
+    if self._field not in RESERVATION_FIELDS:
+      raise KeyError(self._field)
+    super().__init__(entity, name, f"{entity.id}_{self._field}", icon)
     # Fake in some values for testing
-    self._reservation.set('guest_name', 'Veronica')
-    self._reservation.set('door_code',  '0217')
-    self._reservation.set('arrival',    '2026-05-29')
-    self._reservation.set('departure',  '2026-05-30')
-    self._reservation.set('check_in',   '14:00:00')
-    self._reservation.set('check_out',  '12:00:00')
-    self._reservation.set('wifi_ssid',  '4114')
-    self._reservation.set('wifi_key',   'pioneer-plato-carmen')
-
-  async def async_added_to_hass(self):
-    self._reservation.register_callback(self.async_write_ha_state)
-  async def async_will_remove_from_hass(self):
-    self._reservation.remove_callback(self.async_write_ha_state)
-
-  def date_parts(self, dt: datetime) -> dict:
-    return {
-      'datetime': dt.isoformat(),
-      'datetime_abbr': dt.strftime('%Y-%m-%d @ %-I %p'),
-      'date': dt.strftime('%Y-%m-%d'),
-      'date_weekday': dt.strftime('%a, %b %-d'),
-      'datetime_weekday': dt.strftime('%a, %b %-d @ %-I %p'),
-      'time': dt.strftime('%H:%M:%S'),
-      'time_abbr': dt.strftime('%H:%M'),
-      'hour': dt.strftime('%-I %p'),
-      'weekday': dt.strftime('%A'),
-      'weekday_abbr': dt.strftime('%a'),
-      'month': dt.strftime('%B'),
-      'month_abbr': dt.strftime('%b'),
-    }
-
-  @property
-  def device_info(self):
-    return {"identifiers": {(DOMAIN, self._reservation.id)}}
-  @property
-  def available(self) -> bool:
-    return self._reservation.online
+    self._entity.set('guest_name', 'Veronica')
+    self._entity.set('door_code',  '0217')
+    self._entity.set('arrival',    '2026-05-29')
+    self._entity.set('departure',  '2026-05-30')
+    self._entity.set('check_in',   '14:00:00')
+    self._entity.set('check_out',  '12:00:00')
+    self._entity.set('wifi_ssid',  '4114')
+    self._entity.set('wifi_key',   'pioneer-plato-carmen')
   @property
   def state(self):
-    return self._reservation._data[self._id]
-  @property
-  def icon(self):
-    return self._icon
+    return self._entity._data[self._field]
 
-class Guest_Name(ZiBBase):
+class Guest_Name(ZiBSensorBase):
   def __init__(self, reservation: Reservation):
     super().__init__(reservation, 'Guest Name', 'mdi:account-convert')
-    self._attr_name = None # Make Guest_Name the eponymous attribute
-  # Add device-level info (i.e. name) to Guest_Name
+  # Add device-level info (i.e. name)
   @property
   def device_info(self) -> DeviceInfo:
     return super().device_info | {
-      "name": self._reservation.name,
+      "name": self._entity.name,
     }
 
-class Door_Code(ZiBBase):
-  def __init__(self, reservation):
+class Door_Code(ZiBSensorBase):
+  def __init__(self, reservation: Reservation):
     super().__init__(reservation, 'Door Code', 'mdi:door-closed-lock')
 
-class Arrival(ZiBBase):
-  def __init__(self, reservation):
+class Arrival(ZiBSensorBase):
+  def __init__(self, reservation: Reservation):
     super().__init__(reservation, 'Arrival', 'mdi:calendar-start')
     self._attr_device_class = SensorDeviceClass.DATE
   @property
   def extra_state_attributes(self):
     dt = datetime.datetime.fromisoformat(
-        self._reservation.arrival +
-        ' ' + self._reservation.check_in
+        self._entity.arrival +
+        ' ' + self._entity.check_in
       )
-    return self.date_parts(dt)
+    return date_parts(dt)
 
-class Departure(ZiBBase):
-  def __init__(self, reservation):
+class Departure(ZiBSensorBase):
+  def __init__(self, reservation: Reservation):
     super().__init__(reservation, 'Departure', 'mdi:calendar-end')
     self._attr_device_class = SensorDeviceClass.DATE
   @property
   def extra_state_attributes(self):
     dt = datetime.datetime.fromisoformat(
-        self._reservation.departure +
-        ' ' + self._reservation.check_out
+        self._entity.departure +
+        ' ' + self._entity.check_out
       )
-    return self.date_parts(dt)
+    return date_parts(dt)
 
-class Check_In(ZiBBase):
-  def __init__(self, reservation):
+class Check_In(ZiBSensorBase):
+  def __init__(self, reservation: Reservation):
     super().__init__(reservation, 'Check In', 'mdi:timer-check-outline')
 
-class Check_Out(ZiBBase):
-  def __init__(self, reservation):
+class Check_Out(ZiBSensorBase):
+  def __init__(self, reservation: Reservation):
     super().__init__(reservation, 'Check Out', 'mdi:timer-cancel-outline')
 
-class WiFi_SSID(ZiBBase):
-  def __init__(self, reservation):
+class WiFi_SSID(ZiBSensorBase):
+  def __init__(self, reservation: Reservation):
     super().__init__(reservation, 'WiFi SSID', 'mdi:wifi')
 
-class WiFi_Key(ZiBBase):
-  def __init__(self, reservation):
+class WiFi_Key(ZiBSensorBase):
+  def __init__(self, reservation: Reservation):
     super().__init__(reservation, 'WiFi Key', 'mdi:wifi-lock')
   @property
   def extra_state_attributes(self):
     return {
-      'qr': 'WIFI:S:' + self._reservation.wifi_ssid + ';T:WPA;P:' + self._reservation.wifi_key + ';;'
+      'qr': 'WIFI:S:' + self._entity.wifi_ssid + ';T:WPA;P:' + self._entity.wifi_key + ';;'
     }

+ 2 - 2
custom_components/zib_guest/services.yaml

@@ -2,9 +2,9 @@ set_reservation:
   description: Update any set of reservation fields. Fields not specified will not be updated
   target:
     device:
-      integration: zib_guest
+      integration: zib_dev
     entity:
-      integration: zib_guest
+      integration: zib_dev
       domain: sensor
   fields:
     guest_name:

+ 62 - 0
custom_components/zib_guest/switch.py

@@ -0,0 +1,62 @@
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.components.switch import SwitchEntity, SwitchDeviceClass
+from homeassistant.const import STATE_ON, STATE_OFF
+
+from . import ZiBConfigEntry
+from .entity import ZiBEntityBase
+from .const import ROOM_STATE_SWITCHES
+
+async def async_setup_entry(
+  hass: HomeAssistant,
+  config_entry: ZiBConfigEntry,
+  async_add_entities: AddEntitiesCallback,
+) -> None:
+    state = config_entry.runtime_data.state
+    async_add_entities([
+      Guest_Mode(state),
+      Guest_Mode_Auto(state),
+    ])
+
+class ZiBSwitchBase(ZiBEntityBase, SwitchEntity):
+  device_class = SwitchDeviceClass.SWITCH
+
+  def __init__(self, entity: Entity, name: str, icon: str = None):
+    self._field = name.lower().replace(' ', '_')
+    if self._field not in ROOM_STATE_SWITCHES:
+      raise KeyError(self._field)
+    super().__init__(entity, name, f"{entity.id}_{self._field}", icon)
+    # Fake in some values for testing
+    for field in ROOM_STATE_SWITCHES:
+      self._entity.set(field, False)
+  @property
+  def state(self) -> str:
+    return self._entity._data[self._field]
+  @property
+  def is_on(self) -> bool:
+    return (self.state == STATE_ON)
+
+  async def async_set(self, value: bool) -> None:
+    field = self.name.lower().replace(' ', '_')
+    entity = self.platform.config_entry.runtime_data.state
+    entity.set(field, value)
+    await entity.publish_updates()
+  async def async_turn_on(self, **kwargs: Any) -> None:
+    await self.async_set(True)
+  async def async_turn_off(self, **kwargs: Any) -> None:
+    await self.async_set(False)
+
+class Guest_Mode(ZiBSwitchBase):
+  def __init__(self, state: RoomState):
+    super().__init__(state, 'Guest Mode', 'mdi:room-service-outline')
+  # Add device-level info (i.e. name)
+  @property
+  def device_info(self) -> DeviceInfo:
+    return super().device_info | {
+      "name": self._entity.name,
+    }
+
+class Guest_Mode_Auto(ZiBSwitchBase):
+  def __init__(self, state: RoomState):
+    super().__init__(state, 'Guest Mode Auto', 'mdi:refresh-auto')

+ 44 - 0
custom_components/zib_guest/text.py

@@ -0,0 +1,44 @@
+from homeassistant.core import HomeAssistant
+from homeassistant.const import EntityCategory
+from homeassistant.helpers.entity import Entity
+from homeassistant.components.text import TextEntity
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+
+from . import ZiBConfigEntry
+from .entity import ZiBEntityBase
+from .const import ROOM_STATE_TEXTS
+
+async def async_setup_entry(
+  hass: HomeAssistant,
+  config_entry: ZiBConfigEntry,
+  async_add_entities: AddEntitiesCallback,
+) -> None:
+    state = config_entry.runtime_data.state
+    async_add_entities([
+      Reservation_Defaults(state),
+    ])
+
+class ZiBTextBase(ZiBEntityBase, TextEntity):
+  _attr_entity_category = EntityCategory.CONFIG
+
+  def __init__(self, entity: Entity, name: str, icon: str = None):
+    self._field = name.lower().replace(' ', '_')
+    if self._field not in ROOM_STATE_TEXTS:
+      raise KeyError(self._field)
+    super().__init__(entity, name, f"{entity.id}_{self._field}", icon)
+    # Fake in some values for testing
+    for field in ROOM_STATE_TEXTS:
+      self._entity.set(field, False)
+  @property
+  def state(self) -> str:
+    return self._entity._data[self._field]
+
+  async def async_set_value(self, value: str) -> None:
+    field = self.name.lower().replace(' ', '_')
+    entity = self.platform.config_entry.runtime_data.state
+    entity.set(field, value)
+    await entity.publish_updates()
+
+class Reservation_Defaults(ZiBTextBase):
+  def __init__(self, state: RoomState):
+    super().__init__(state, 'Reservation Defaults', 'mdi:format-list-group')