Кратко

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)


  1. xverizex
    04.05.2022 17:56

    Ничего себе. Видно что статью прочитал один человек, а плюсика уже два поставили.


  1. delphinpro
    04.05.2022 18:01
    +2

    Я, конечно, извиняюсь, но где статья?
    Я вижу только вступление, ссылку на гитхаб и кусок кода с этого самого гитхаба.


  1. oldd
    04.05.2022 20:17
    +1

    for (sensorId in 0 until sensors.length()) {

    Это чтоб враг не догадался? )) Он-то думает, что sensorId - это идентификатор, а это просто декрементный счётчик


    1. randomsimplenumber
      05.05.2022 08:03
      +1

      "Некоторые части кода возможно были выполнены небрежно" (ц)