Кратко
Smart System это небольшое и простое в использовании приложение для умного дома с простой структурой, написанное на kotlin.
Цель этого приложения — сделать удаленное выполнение функций максимально простым и удобным для пользователя, чтобы помочь начать работу с технологией умного дома.
Технологический стек самый стандартный - kotlin(backend), xml(frontend).
Поддерживаемые устройства
Smart System поддерживает следующие девайсы:
Philips Hue Bridge
Shelly
Devices using ESP Easy
Devices using Tasmota
Devices using the SimpleHome API
Activities
Главный экран содержит меню управления и подключения к устройствам умного дома.
В настройках есть возможность изменить тему приложения, возможность изменить расположение списка объектов, кнопка изменения списка объектов и удаление всех сразу.
Экран добавления объекта содержит имя , адрес объекта , выбор типа объекта и иконки и выбор модулей.
Скриншоты
Backend
Я остановлюсь наверное на самых важных и интересных частях кода - это RETROFIT. Как я писал в самом начале, мое приложение работает с определенными модулями(ESP, Hue, Shelly, SimpleHome, Tasmota). Поэтому в пакете resources находятся JSON объекты этих модулей. На примере ESPeasy, я покажу суть всей работы моего приложения с API.ё
В файле EspEasyAPI.kt и EspEasyAPIParser.kt (Думаю эти незамысловатые названия говорят сами за себя)
class EspEasyAPI(
c: Context,
deviceId: String,
recyclerViewInterface: HomeRecyclerViewHelperInterface?
) : UnifiedAPI(c, deviceId, recyclerViewInterface) {
private val parser = EspEasyAPIParser(c.resources, this)
override fun loadList(callback: CallbackInterface) {
val jsonObjectRequest = JsonObjectRequest(
Request.Method.GET, url + "json", null,
{ infoResponse ->
callback.onItemsLoaded(
UnifiedRequestCallback(
parser.parseResponse(infoResponse),
deviceId
),
recyclerViewInterface
)
},
{ error ->
callback.onItemsLoaded(
UnifiedRequestCallback(null, deviceId,
Global.volleyError(c, error)
), null)
}
)
queue.add(jsonObjectRequest)
}
override fun loadStates(callback: RealTimeStatesCallback, offset: Int) {
val jsonObjectRequest = JsonObjectRequest(
Request.Method.GET, url + "json", null,
{ infoResponse ->
callback.onStatesLoaded(
parser.parseStates(infoResponse),
offset,
dynamicSummaries
)
}, { }
)
queue.add(jsonObjectRequest)
}
override fun changeSwitchState(id: String, state: Boolean) {
val switchUrl = url + "control?cmd=GPIO," + id + "," + (if (state) "1" else "0")
val jsonObjectRequest = JsonObjectRequest(
switchUrl,
{ },
{ e -> Log.e(Global.LOG_TAG, e.toString()) }
)
queue.add(jsonObjectRequest)
}
}
class EspEasyAPIParser(resources: Resources, api: UnifiedAPI?) : UnifiedAPI.Parser(resources, api) {
override fun parseResponse(response: JSONObject): ArrayList<ListViewItem> {
val listItems = arrayListOf<ListViewItem>()
//sensors
val sensors = response.optJSONArray("Sensors") ?: JSONArray()
for (sensorId in 0 until sensors.length()) {
val currentSensor = sensors.getJSONObject(sensorId)
if (currentSensor.optString("TaskEnabled", "false").equals("false")) {
continue
}
val type = currentSensor.optString("Type")
if (type.startsWith("Environment")) {
parseEnvironment(listItems, type, currentSensor)
} else if (type.startsWith("Switch")) {
parseSwitch(listItems, type, currentSensor)
}
}
return listItems
}
private fun parseEnvironment(listItems: ArrayList<ListViewItem>, type: String, currentSensor: JSONObject) {
var taskIcons = intArrayOf()
when (type) {
"Environment - BMx280" -> {
taskIcons += R.drawable.ic_device_thermometer
taskIcons += R.drawable.ic_device_hygrometer
taskIcons += R.drawable.ic_device_gauge
}
"Environment - DHT11/12/22 SONOFF2301/7021" -> {
taskIcons += R.drawable.ic_device_thermometer
taskIcons += R.drawable.ic_device_hygrometer
}
"Environment - DS18b20" -> {
taskIcons += R.drawable.ic_device_thermometer
}
}
val taskName = currentSensor.getString("TaskName")
for (taskId in taskIcons.indices) {
val currentTask = currentSensor.getJSONArray("TaskValues").getJSONObject(taskId)
val currentValue = currentTask.getString("Value")
if (!currentValue.equals("nan")) {
val suffix = when (taskIcons[taskId]) {
R.drawable.ic_device_thermometer -> " °C"
R.drawable.ic_device_hygrometer -> " %"
R.drawable.ic_device_gauge -> " hPa"
else -> ""
}
listItems += ListViewItem(
title = currentValue + suffix,
summary = taskName + ": " + currentTask.getString("Name"),
icon = taskIcons[taskId]
)
}
}
}
private fun parseSwitch(listItems: ArrayList<ListViewItem>, type: String, currentSensor: JSONObject) {
when (type) {
"Switch input - Switch" -> {
val currentState = currentSensor.getJSONArray("TaskValues").getJSONObject(0).getInt("Value") > 0
var taskName =currentSensor.getString("TaskName")
var gpioId = ""
val gpioFinder = Regex("~GPIO~([0-9]+)$")
val matchResult = gpioFinder.find(taskName)
if (matchResult != null && matchResult.groupValues.size > 1) {
gpioId = matchResult.groupValues[1]
taskName = taskName.replace("~GPIO~$gpioId", "")
}
listItems += ListViewItem(
title = taskName,
summary = resources.getString(
if (currentState) R.string.switch_summary_on
else R.string.switch_summary_off
),
hidden = gpioId,
state = currentState,
icon = R.drawable.ic_do
)
api?.needsRealTimeData = true
}
}
}
override fun parseStates(response: JSONObject): ArrayList<Boolean?> {
val listItems = arrayListOf<Boolean?>()
//sensors
val sensors = response.optJSONArray("Sensors") ?: JSONArray()
for (sensorId in 0 until sensors.length()) {
val currentSensor = sensors.getJSONObject(sensorId)
if (currentSensor.optString("TaskEnabled", "false").equals("false")) {
continue
}
val type = currentSensor.optString("Type")
if (type.startsWith("Environment")) {
parseEnvironmentStates(listItems, type, currentSensor)
} else if (type.startsWith("Switch")) {
parseSwitchStates(listItems, type, currentSensor)
}
}
return listItems
}
private fun parseEnvironmentStates(listItems: ArrayList<Boolean?>, type: String, currentSensor: JSONObject) {
var tasks = 0
when (type) {
"Environment - BMx280" -> tasks += 3
"Environment - DHT11/12/22 SONOFF2301/7021" -> tasks += 2
"Environment - DS18b20" -> tasks++
}
for (taskId in 0 until tasks) {
if (
!currentSensor.getJSONArray("TaskValues")
.getJSONObject(taskId)
.getString("Value")
.equals("nan")
) listItems += null
}
}
private fun parseSwitchStates(listItems: ArrayList<Boolean?>, type: String, currentSensor: JSONObject) {
when (type) {
"Switch input - Switch" -> {
listItems += currentSensor.getJSONArray("TaskValues").getJSONObject(0).getInt("Value") > 0
}
}
}
}
Если кому-то интересно как я реализовал остальные аспекты работы приложения на kotlin, то можно ознакомиться с кодом подробнее по ссылке ниже.
Приложение было разработано за два дня в рамках хакатона be-coder с тематикой Системы умного дома. Некоторые части кода возможно были выполнены небрежно и требуют доработки, так что буду рад выслушать советы и конструктивную критику!)
Комментарии (4)
delphinpro
04.05.2022 18:01+2Я, конечно, извиняюсь, но где статья?
Я вижу только вступление, ссылку на гитхаб и кусок кода с этого самого гитхаба.
oldd
04.05.2022 20:17+1for (sensorId in 0 until sensors.length()) {
Это чтоб враг не догадался? )) Он-то думает, что
sensorId - это идентификатор, а это просто декрементный счётчик
xverizex
Ничего себе. Видно что статью прочитал один человек, а плюсика уже два поставили.