Skip to content

Commit

Permalink
added get_open_sensors functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
jbaruch committed Oct 23, 2024
1 parent de3b408 commit bbe7014
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 51 deletions.
49 changes: 25 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
## Quick Start
1. Set up Maker API on `hubitat.local` or use `DEFAULT_HUB_IP` environment variable to specify the IP of your primary hub.
2. Configure mandatory environment variables: `MAKER_API_APP_ID`, `MAKER_API_TOKEN`, `BOT_TOKEN`.
* If you know the ID of your DM chat with your bot, you can add it as `CHAT_ID` variable.
* If your primary hub is not using `hubitat.local` DNS entry, use `DEFAULT_HUB_IP` to specify the ip.
* If you know the ID of your DM chat with your bot, you can add it as `CHAT_ID` variable.
* If your primary hub is not using `hubitat.local` DNS entry, use `DEFAULT_HUB_IP` to specify the ip.
3. Deploy the Docker container.
4. Start sending commands to your bot!

Expand All @@ -21,12 +21,14 @@
* `/push [device name]` - Executes a push action on a button device.
* `/hold [device name]` - Executes a hold action on a button device.
* `/release [device name]` - Executes a release action on a button device.
* `/get_open_sensors` - Lists all sensors that are currently open.

* System Commands:
* `/cancel_alerts` - Cancels all alerts in HSM.
* `/update` - Updates all hubs for which Hub Information Drivers are exposed in the Maker API of `hubitat.local`.
* `/reboot [Hub Information Driver v3 instance name]` - Reboots a specified hub.


## Device Name Notations:
You can refer to devices in several ways:
* **Full name:** Use the complete device name.
Expand All @@ -48,32 +50,31 @@ You can refer to devices in several ways:
4. Expose all needed devices in Maker API (including devices from other hubs via mesh).
5. Ensure Hub Information Driver v3 is installed on every hub (via Hubitat Package Manager) and exposed in Maker API on `hubitat.local` (directly and via mesh).
6. Create a bot in Telegram using BotFather bot.
7. Configure environment variables:
* Mandatory:
* `MAKER_API_APP_ID` - The Maker API app ID.
* `MAKER_API_TOKEN` - The Maker API token.
* `BOT_TOKEN` - The Telegram bot token.
* Optional:
* `CHAT_ID` - The ID of your DM chat with the Bot (or other chat the bot is added to).
If you use that variable, the bot will be able to proactively send information to the chat. If you don't use it, it will only reply to your messages.
There are number of ways to find this ID, some of them are listed [here](https://stackoverflow.com/questions/32423837/telegram-bot-how-to-get-a-group-chat-id).
* `DEFAULT_HUB_IP` – The IP of the hub the Maker API app is installed on. Defaults to DNS hostname of `hubitat.local`.
8. Deploy the Docker image:
- Load the Docker image: `docker load < tg-hubitat-bot-docker-image.tar`
- Create and run the container: `docker run -d --name tg-hubitat-bot -e MAKER_API_APP_ID -e MAKER_API_TOKEN -e BOT_TOKEN tg-hubitat-bot`
7. (Optional) create commands in the bot using BotFather's `/setcommands` command. This is optional as you can send arbitrary commands by text without completion in the bot.
8. Configure environment variables:
* Mandatory:
* `MAKER_API_APP_ID` - The Maker API app ID.
* `MAKER_API_TOKEN` - The Maker API token.
* `BOT_TOKEN` - The Telegram bot token.
* Optional:
* `CHAT_ID` - The ID of your DM chat with the Bot (or other chat the bot is added to).
If you use that variable, the bot will be able to proactively send information to the chat. If you don't use it, it will only reply to your messages.
There are number of ways to find this ID, some of them are listed [here](https://stackoverflow.com/questions/32423837/telegram-bot-how-to-get-a-group-chat-id).
* `DEFAULT_HUB_IP` – The IP of the hub the Maker API app is installed on. Defaults to DNS hostname of `hubitat.local`.
9. Deploy the Docker image:
- Load the Docker image: `docker load < tg-hubitat-bot-docker-image.tar`
- Create and run the container: `docker run -d --name tg-hubitat-bot -e MAKER_API_APP_ID -e MAKER_API_TOKEN -e BOT_TOKEN tg-hubitat-bot`

## Adding New Device Types:
To add support for new device types:
1. Update the `Device` sealed class in `Device.kt` to include the new device type.
2. Add the appropriate `supportedOps` for the new device type.
3. If necessary, update the `DeviceManager` to handle any special cases for the new device type.
3. Define the appropriate `attributes` for the new device type.
4. If necessary, update the `DeviceManager` to handle any special cases for the new device type, including how to query and interpret the new attributes.
5. Test the new device type to ensure that all operations and attribute queries work as expected.

## Building:
* `./gradlew build` - Build the project.
* `./gradlew jibDockerBuild` - Build a Docker container.
* `./gradlew jibBuildTar` - Create a portable tar of the container.
### Build the project
1. Build the project using `./gradlew build`.
2. Create the Docker image using `./gradlew jibDockerBuild`
3. (Optional) Export the image to tar file using `./gradlew jibBuildTar`

## Additional Resources
* [Hubitat Maker API Documentation](https://docs.hubitat.com/index.php?title=Maker_API)
* [Telegram BotFather](https://core.telegram.org/bots#botfather)
* [Hubitat Community Forums](https://community.hubitat.com/)
78 changes: 61 additions & 17 deletions src/main/kotlin/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import java.lang.System.getenv
import com.github.kotlintelegrambot.logging.LogLevel
import com.github.kotlintelegrambot.entities.ParseMode.MARKDOWN_V2
import com.github.kotlintelegrambot.extensions.filters.Filter
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText

private val BOT_TOKEN = getenv("BOT_TOKEN") ?: throw IllegalStateException("BOT_TOKEN not set")
private val MAKER_API_APP_ID = getenv("MAKER_API_APP_ID") ?: throw IllegalStateException("MAKER_API_APP_ID not set")
Expand Down Expand Up @@ -68,29 +70,55 @@ fun main() {
}
command("refresh") {
val refreshResults = deviceManager.refreshDevices(getDevicesJson())
bot.sendMessage(chatId = ChatId.fromId(message.chat.id), text = "Refresh finished, ${refreshResults.first} devices loaded. Warnings: ${refreshResults.second}")
bot.sendMessage(
chatId = ChatId.fromId(message.chat.id),
text = "Refresh finished, ${refreshResults.first} devices loaded. Warnings: ${refreshResults.second}"
)
}
command("list") {
bot.sendMessage(chatId = ChatId.fromId(message.chat.id), text = deviceManager.list(), parseMode = MARKDOWN_V2)
bot.sendMessage(
chatId = ChatId.fromId(message.chat.id),
text = deviceManager.list(),
parseMode = MARKDOWN_V2
)
}
command("shutdown_restart") {
//find hubs with z-wave on and iterate on them

//var zWaveHubs = deviceManager.findZWaveEnabledHubs()
//zWaveHubs.foreach{
// runDeviceCommand(it, "shutdown")
// bot.sendMessage(chatId = ChatId.fromId(message.chat.id), text = "Shutting down, please wait for graceful shutdown.")
// TimeUnit.MINUTES.sleep(1)
// bot.sendMessage(chatId = ChatId.fromId(message.chat.id), text = "Cutting power, please wait for radios reset.")
// TimeUnit.MINUTES.sleep(1)
// bot.sendMessage(chatId = ChatId.fromId(message.chat.id), text = "Restarting hub.")
// runDeviceCommand(it, "shutdown")
// bot.sendMessage(chatId = ChatId.fromId(message.chat.id), text = "Shutting down, please wait for graceful shutdown.")
// TimeUnit.MINUTES.sleep(1)
// bot.sendMessage(chatId = ChatId.fromId(message.chat.id), text = "Cutting power, please wait for radios reset.")
// TimeUnit.MINUTES.sleep(1)
// bot.sendMessage(chatId = ChatId.fromId(message.chat.id), text = "Restarting hub.")
//}
}
command("get_open_sensors") {
val openSensors = deviceManager.findDevicesByType(Device.ContactSensor::class.java)
.mapNotNull { sensor ->
val currentValue = getDeviceAttribute(sensor, "contact")
if (currentValue == "open") {
sensor.label
} else {
null
}
}.joinToString(separator = "\n")

val response = if (openSensors.isNotEmpty()) {
"Open Sensors:\n$openSensors"
} else {
"No open sensors found."
}

bot.sendMessage(chatId = ChatId.fromId(message.chat.id), text = response)
}
}
}

println("Init successful, $deviceManager devices loaded, start polling")
if(CHAT_ID != "") {
if (CHAT_ID != "") {
bot.sendMessage(
chatId = ChatId.fromId(CHAT_ID.toLong()),
text = "Init successful, $deviceManager devices loaded, start polling"
Expand All @@ -105,9 +133,10 @@ fun String.snakeToCamelCase(): String {
}.joinToString("")
}

private suspend fun getDevicesJson(): String = client.get("http://${DEFAULT_HUB_IP}/apps/api/${MAKER_API_APP_ID}/devices") {
parameter("access_token", MAKER_API_TOKEN)
}.body()
private suspend fun getDevicesJson(): String =
client.get("http://${DEFAULT_HUB_IP}/apps/api/${MAKER_API_APP_ID}/devices") {
parameter("access_token", MAKER_API_TOKEN)
}.body()


suspend fun handleDeviceCommand(bot: Bot, message: Message) {
Expand Down Expand Up @@ -142,19 +171,34 @@ suspend fun handleDeviceCommand(bot: Bot, message: Message) {
bot.sendMessage(chatId = ChatId.fromId(message.chat.id), text = result)
}

suspend fun runDeviceCommand(device: Device, command: String, args: List<String>): String {
val commandPath = buildString {
append("/apps/api/${MAKER_API_APP_ID}/devices/${device.id}/$command")

suspend fun runDeviceQuery(device: Device, path: String, args: List<String> = emptyList()): HttpResponse {
val fullPath = buildString {
append("/apps/api/${MAKER_API_APP_ID}/devices/${device.id}/$path")
if (args.isNotEmpty()) {
append("/${args.joinToString("/")}")
}
}

return client.get("http://${DEFAULT_HUB_IP}$commandPath") {
return client.get("http://${DEFAULT_HUB_IP}$fullPath") {
parameter("access_token", MAKER_API_TOKEN)
}.status.description
}
}

suspend fun runDeviceCommand(device: Device, command: String, args: List<String>): String {
val response = runDeviceQuery(device, command, args)
return response.status.description
}

suspend fun getDeviceAttribute(device: Device, attribute: String): String {
val response = runDeviceQuery(device, "attribute/$attribute")
val body = response.bodyAsText()
val json = Json.parseToJsonElement(body).jsonObject
return json["value"]?.jsonPrimitive?.content ?: "Unknown"
}



suspend fun runCommandOnHsm(command: String): String {
return client.get("http://${DEFAULT_HUB_IP}/apps/api/${MAKER_API_APP_ID}/hsm/$command") {
parameter("access_token", MAKER_API_TOKEN)
Expand Down
49 changes: 39 additions & 10 deletions src/main/kotlin/model/Device.kt
Original file line number Diff line number Diff line change
@@ -1,67 +1,96 @@
@file:Suppress("unused")

package jbaru.ch.telegram.hubitat.model
import io.ktor.util.Attributes
import kotlinx.serialization.*

@Serializable
sealed class Device {
abstract val id: Int
abstract val label: String
abstract val supportedOps: Map<String, Int>

abstract val attributes: Map<String, List<String>>

@Serializable
sealed class Actuator() : Device() {
override val supportedOps: Map<String, Int> = mapOf("on" to 0, "off" to 0)
override val attributes: Map<String, List<String>> = emptyMap()

}

@Serializable
sealed class Button() : Device() {
override val supportedOps: Map<String, Int> = mapOf("doubleTap" to 1, "hold" to 1, "push" to 1, "release" to 1)
override val attributes: Map<String, List<String>> = emptyMap()
}

@Serializable
sealed class Shade() : Device() {
override val supportedOps: Map<String, Int> = mapOf("open" to 0, "close" to 0)
override val attributes: Map<String, List<String>> = emptyMap()
}

@Serializable
sealed class Sensor() : Device() {
override val supportedOps: Map<String, Int> = emptyMap()
}

@Serializable
sealed class ContactSensor() : Sensor() {
override val attributes: Map<String, List<String>> = mapOf("contact" to listOf("open", "closed"))
}

@Serializable
@SerialName("Generic Zigbee Contact Sensor")
data class GenericZigbeeContactSensor(override val id: Int, override val label: String) : ContactSensor()

@Serializable
@SerialName("SmartThings Multipurpose Sensor V5")
data class SmartThingsMultipurposeSensorV5(override val id: Int, override val label: String) : ContactSensor()

@Serializable
@SerialName("Hub Information Driver v3")
data class Hub(override val id:Int, override val label: String, var managementToken:String = "", var ip:String = "") : Device() {
data class Hub(
override val id: Int,
override val label: String,
var managementToken: String = "",
var ip: String = ""
) : Device() {
override val supportedOps: Map<String, Int> = mapOf("reboot" to 0)
override val attributes: Map<String, List<String>> = emptyMap()
}

@Serializable
@SerialName("Virtual Switch")
data class VirtualSwitch (override val id: Int, override val label: String) : Actuator()
data class VirtualSwitch(override val id: Int, override val label: String) : Actuator()

@Serializable
@SerialName("Virtual Button")
data class VirtualButton (override val id: Int, override val label: String) : Button()
data class VirtualButton(override val id: Int, override val label: String) : Button()

@Serializable
@SerialName("Room Lights Activator Switch")
data class RoomLightsActivatorSwitch (override val id: Int, override val label: String) : Actuator()
data class RoomLightsActivatorSwitch(override val id: Int, override val label: String) : Actuator()

@Serializable
@SerialName("Room Lights Activator Dimmer")
data class RoomLightsActivatorDimmer (override val id: Int, override val label: String) : Actuator()
data class RoomLightsActivatorDimmer(override val id: Int, override val label: String) : Actuator()

@Serializable
@SerialName("Room Lights Activator Bulb")
data class RoomLightsActivatorBulb (override val id: Int, override val label: String) : Actuator()
data class RoomLightsActivatorBulb(override val id: Int, override val label: String) : Actuator()

@Serializable
@SerialName("Generic Zigbee Outlet")
data class GenericZigbeeOutlet (override val id: Int, override val label: String) : Actuator()
data class GenericZigbeeOutlet(override val id: Int, override val label: String) : Actuator()

@Serializable
@SerialName("Zooz Zen27 Central Scene Dimmer")
data class ZoozDimmer (override val id: Int, override val label: String) : Actuator()
data class ZoozDimmer(override val id: Int, override val label: String) : Actuator()

@Serializable
@SerialName("Zooz Zen76 S2 Switch")
data class ZoozSwitch (override val id: Int, override val label: String) : Actuator()
data class ZoozSwitch(override val id: Int, override val label: String) : Actuator()

@Serializable
@SerialName("Room Lights Activator Shade")
Expand Down

0 comments on commit bbe7014

Please sign in to comment.