diff --git a/backend/app.py b/backend/app.py index d1de867..9ba87da 100644 --- a/backend/app.py +++ b/backend/app.py @@ -24,7 +24,6 @@ import re import os from datetime import datetime -from sqlalchemy.orm import class_mapper from database import db, User, Projeto, Area, Ambiente, Circuito, Modulo, Vinculacao, Keypad, KeypadButton, QuadroEletrico, Cena, Acao, CustomAcao app = Flask(__name__, instance_relative_config=True) @@ -4070,57 +4069,6 @@ def delete_cena(cena_id): return jsonify({"ok": True}) -def serialize_model(obj): - """Função auxiliar para serializar objetos SQLAlchemy para dicionários.""" - if obj is None: - return None - - data = {} - # Itera sobre as colunas do modelo - for c in obj.__table__.columns: - # Formata datas para ISO 8601 - if isinstance(getattr(obj, c.name), datetime): - data[c.name] = getattr(obj, c.name).isoformat() - else: - data[c.name] = getattr(obj, c.name) - - # Itera sobre os relacionamentos definidos no modelo - for rel in class_mapper(obj.__class__).relationships: - related_obj = getattr(obj, rel.key) - if related_obj is not None: - if rel.uselist: # Se for uma lista de objetos (one-to-many) - data[rel.key] = [serialize_model(item) for item in related_obj] - else: # Se for um único objeto (many-to-one) - data[rel.key] = serialize_model(related_obj) - else: - data[rel.key] = None if rel.uselist else None # Lista vazia ou objeto nulo - - return data - -@app.route('/api/export-project-data/') -@login_required -def export_project_data(projeto_id): - """Exporta todos os dados de um projeto em formato JSON para teste.""" - projeto = Projeto.query.options( - joinedload('*') # Carrega todos os relacionamentos de forma recursiva (cuidado com performance) - ).get(projeto_id) - - if not projeto: - return jsonify({"erro": "Projeto não encontrado"}), 404 - - if projeto.user_id != current_user.id and current_user.role != 'admin': - return jsonify({"ok": False, "error": "Acesso negado."}), 403 - - # Serializa o projeto e seus relacionamentos para um dicionário - projeto_dict = serialize_model(projeto) - - # Retorna o dicionário como uma resposta JSON - return Response( - json.dumps(projeto_dict, indent=2, ensure_ascii=False), - mimetype="application/json", - headers={"Content-Disposition": f"attachment;filename=projeto_teste_{projeto.id}.json"} - ) - @app.route("/", defaults={"path": ""}) @app.route("/") def spa_catch_all(path): diff --git a/backend/standalone_roehn_converter.py b/backend/standalone_roehn_converter.py index 43b5b39..f7eb983 100644 --- a/backend/standalone_roehn_converter.py +++ b/backend/standalone_roehn_converter.py @@ -98,7 +98,7 @@ def _create_controller_module(self, controller_type, project_info): "DevID": 1, "ACNET_SlotCapacity": 24, "Scene_SlotCapacity": 96, - "UnitIds": [39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57] + "UnitIds": [58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76] }, "ADP-M8": { "Name": "ADP-M8", @@ -106,7 +106,7 @@ def _create_controller_module(self, controller_type, project_info): "DevID": 3, "ACNET_SlotCapacity": 250, "Scene_SlotCapacity": 256, - "UnitIds": [59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77] + "UnitIds": [59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77] # Estes IDs podem precisar de ajuste }, "ADP-M16": { "Name": "ADP-M16", @@ -114,7 +114,7 @@ def _create_controller_module(self, controller_type, project_info): "DevID": 5, "ACNET_SlotCapacity": 250, "Scene_SlotCapacity": 256, - "UnitIds": [104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122] + "UnitIds": [104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122] # Estes IDs podem precisar de ajuste } } @@ -161,18 +161,18 @@ def _create_controller_module(self, controller_type, project_info): controller_module = { "$type": "Module", - "Name": config["Name"], + "Name": project_info.get('nome') or config["Name"], "DriverGuid": config["DriverGuid"], "Guid": str(uuid.uuid4()), - "IpAddress": project_info.get('m4_ip'), - "HsnetAddress": int(project_info.get('m4_hsnet') or 245), + "IpAddress": project_info.get('ip_address'), + "HsnetAddress": int(project_info.get('hsnet') or 245), "PollTiming": 0, "Disabled": False, "RemotePort": 0, - "RemoteIpAddress": project_info.get('m4_ip'), + "RemoteIpAddress": project_info.get('ip_address'), "Notes": None, "Logicserver": True, - "DevID": config["DevID"], + "DevID": project_info.get('dev_id') or config["DevID"], "DevIDSlave": 0, "UnitComposers": unit_composers, "Slots": [ @@ -210,6 +210,7 @@ def convert_project_from_json(self, project_json): print(f"Numero de areas: {len(project_json.get('areas', []))}") self._circuit_guid_map = {} + self._scene_guid_map = {} self._quadro_guid_map = {} self._room_guid_map = {} main_controller_id = None @@ -224,60 +225,30 @@ def convert_project_from_json(self, project_json): quadro_guid = self._ensure_automation_board_exists(area.get('nome'), ambiente.get('nome'), quadro.get('nome')) self._quadro_guid_map[quadro.get('id')] = quadro_guid - # Etapa 1: Encontrar o controlador "Logic Server" e colocá-lo no quadro correto - logic_server_module = None - for modulo in project_json.get('modulos', []): - if modulo.get('is_logic_server'): - logic_server_module = modulo - break + # Etapa 1 e 2: Processar todos os módulos (controladores ou não) - if logic_server_module: - print(f"Logic Server encontrado: {logic_server_module.get('nome')} ({logic_server_module.get('tipo')})") - main_controller_id = logic_server_module.get('id') - controller_info = { - 'm4_ip': logic_server_module.get('ip_address'), - 'm4_hsnet': logic_server_module.get('hsnet'), - 'm4_devid': logic_server_module.get('dev_id'), - } - controller_module_json = self._create_controller_module(logic_server_module.get('tipo'), controller_info) + # Primeiro, limpa a lista de módulos padrão + self.project_data["Areas"][0]["SubItems"][0]["AutomationBoards"][0]["ModulesList"] = [] - # Remover a controladora padrão que vem na criação do projeto - default_board_modules = self.project_data["Areas"][0]["SubItems"][0]["AutomationBoards"][0]["ModulesList"] - self.project_data["Areas"][0]["SubItems"][0]["AutomationBoards"][0]["ModulesList"] = [ - m for m in default_board_modules if m.get("Logicserver") is not True - ] + all_modules = project_json.get('modulos', []) + for modulo in all_modules: + quadro_guid = self._quadro_guid_map.get(modulo.get('quadro_eletrico_id')) - # Encontrar o quadro elétrico associado ao controlador - target_board_id = logic_server_module.get('quadro_eletrico_id') - if target_board_id and target_board_id in self._quadro_guid_map: - target_board_guid = self._quadro_guid_map[target_board_id] - target_board_json = self._find_automation_board_by_guid(target_board_guid) + if modulo.get('is_controller'): + # Cria o módulo controlador com as informações corretas + controller_module_json = self._create_controller_module(modulo.get('tipo'), modulo) + controller_module_json['Logicserver'] = modulo.get('is_logic_server', False) + + # Aloca no quadro correto + target_board_json = self._find_automation_board_by_guid(quadro_guid) if quadro_guid else None if target_board_json: - # Encontrar nome do quadro para log - quadro_nome = "Desconhecido" - for area in project_json.get('areas', []): - for amb in area.get('ambientes', []): - for quad in amb.get('quadros_eletricos', []): - if quad.get('id') == target_board_id: - quadro_nome = quad.get('nome') - break - print(f"Logic Server alocado no quadro: {quadro_nome}") - target_board_json.setdefault("ModulesList", []).insert(0, controller_module_json) + target_board_json.setdefault("ModulesList", []).append(controller_module_json) else: - print(f"AVISO: Quadro com GUID {target_board_guid} não encontrado. Alocando no quadro padrão.") - self.project_data["Areas"][0]["SubItems"][0]["AutomationBoards"][0]["ModulesList"].insert(0, controller_module_json) + # Fallback para o quadro padrão + self.project_data["Areas"][0]["SubItems"][0]["AutomationBoards"][0]["ModulesList"].append(controller_module_json) else: - print("Logic Server não associado a um quadro. Alocando no quadro padrão.") - self.project_data["Areas"][0]["SubItems"][0]["AutomationBoards"][0]["ModulesList"].insert(0, controller_module_json) - else: - print("ERRO CRÍTICO: Nenhum Logic Server encontrado no projeto. O arquivo RWP pode estar incompleto.") - - # Etapa 2: Processar todos os outros módulos (controladores ou não) - all_modules = [m for m in project_json.get('modulos', []) if m.get('id') != main_controller_id] - - for modulo in all_modules: - quadro_guid = self._quadro_guid_map.get(modulo.get('quadro_eletrico_id')) - self._ensure_module_exists(modulo, automation_board_guid=quadro_guid) + # Garante que módulos não-controladores sejam criados + self._ensure_module_exists(modulo, automation_board_guid=quadro_guid) # Etapa 3: Processar todos os circuitos e criar seus GUIDs e links físicos for area in project_json.get('areas', []): @@ -325,17 +296,17 @@ def convert_project_from_json(self, project_json): traceback.print_exc() continue - # Etapa 4: Processar Keypads e Cenas + # Etapa 4: Processar Cenas e depois Keypads for area in project_json.get('areas', []): for ambiente in area.get('ambientes', []): - self._add_keypads_for_room(area.get('nome'), ambiente) self._add_scenes_for_room(area.get('nome'), ambiente) + self._add_keypads_for_room(area.get('nome'), ambiente) # Etapa 5: Verificação final do ACNET print("Realizando verificação final do ACNET...") self._verify_and_fix_acnet(project_json) - # ⭐⭐⭐ NOVO: Log do estado final do ACNET + # Log do estado final do ACNET self._log_acnet_status() print("✅ Processamento do projeto concluído!") @@ -1304,11 +1275,16 @@ def make_composer(name, port_number, port_type, kind, io): primary_ports = [1, 2, 3, 4] secondary_ports = [5, 6, 7, 8] - for button in sorted(keypad.get('buttons', []), key=lambda b: b.get('ordem') or 0): - ordem = button.get('ordem') - if ordem and ordem > button_count: + buttons_data = list(keypad.get('buttons', [])) + while len(buttons_data) < button_count: + buttons_data.append({'button_index': len(buttons_data) + 1}) + + for button in sorted(buttons_data, key=lambda b: b.get('button_index', 0)): + button_index = button.get('button_index') + if not button_index or button_index > button_count: continue - index = (ordem - 1) if ordem else 0 + + index = button_index - 1 primary_port = primary_ports[index % len(primary_ports)] secondary_port = secondary_ports[index % len(secondary_ports)] @@ -1317,21 +1293,39 @@ def make_composer(name, port_number, port_type, kind, io): unit_secondary_key = make_composer("UnitSecondaryKey", secondary_port, 300, 0, 0) unit_secondary_led = make_composer("UnitSecondaryLed", secondary_port, 200, 1, 1) + # --- Nova Lógica para ler a configuração do botão --- target_guid = zero_guid - circuito = button.get('circuito') - cena = button.get('cena') - - if cena: - target_guid = cena.get('guid') - print(f" - Button {ordem}: Linked to scene '{cena.get('nome')}' (ID: {cena.get('id')}) -> GUID: {target_guid}") - elif circuito and circuito.get('id') in self._circuit_guid_map: - target_guid = self._circuit_guid_map[circuito.get('id')] - print(f" - Button {ordem}: Linked to circuit '{circuito.get('nome')}' (ID: {circuito.get('id')}) -> GUID: {target_guid}") + modo = 3 # Default Toggle + command_on = 1 + command_off = 0 + engraver_text = "" + + json_config = button.get('json_config', {}) + action = json_config.get('action', {}) + engraver_text = json_config.get('EngraverText', '') + + target_type = action.get('target_type') + target_id = action.get('target_id') + action_type = action.get('type') + + if action_type == 'Toggle': + modo = 3 + command_on = 1 + command_off = 0 + # Adicionar mais tipos de ação aqui se necessário + + if target_type == 'circuito' and target_id is not None and target_id in self._circuit_guid_map: + target_guid = self._circuit_guid_map[target_id] + print(f" - Button {button_index}: Linked to circuit ID {target_id} -> GUID: {target_guid}") + elif target_type == 'cena' and target_id is not None and target_id in self._scene_guid_map: + target_guid = self._scene_guid_map[target_id] + modo = 1 # Ativar Cena + command_on = 0 + command_off = 0 + print(f" - Button {button_index}: Linked to scene ID {target_id} -> GUID: {target_guid}") else: - if circuito: - print(f" - Button {ordem}: WARNING - Circuit '{circuito.get('nome')}' (ID: {circuito.get('id')}) found but its GUID is not in the map.") - else: - print(f" - Button {ordem}: Not linked.") + print(f" - Button {button_index}: Not linked.") + # --- Fim da Nova Lógica --- style_properties = None button_style_guid = zero_guid @@ -1370,9 +1364,9 @@ def make_composer(name, port_number, port_type, kind, io): "CanHold": bool(button.get('can_hold')), "Guid": button.get('guid') or str(uuid.uuid4()), "TargetObjectGuid": target_guid, - "Modo": button.get('modo'), - "CommandOn": button.get('command_on'), - "CommandOff": button.get('command_off'), + "Modo": modo, + "CommandOn": command_on, + "CommandOff": command_off, "PortNumber": 0, "UnitControleLed": 0, "LedColor": 0, @@ -1383,7 +1377,7 @@ def make_composer(name, port_number, port_type, kind, io): "UnitSecondaryKey": unit_secondary_key, "UnitSecondaryLed": unit_secondary_led, "ButtonStyleGuid": button_style_guid, - "EngraverText": button.get('engraver_text'), + "EngraverText": engraver_text, "Automode": True, } payload["ListKeypadButtons"].append(button_payload) @@ -1725,9 +1719,13 @@ def _add_scenes_for_room(self, area_name, ambiente): for cena_data in cenas: next_unit_id = self._find_max_unit_id() + 1 + cena_guid = cena_data.get('guid') or str(uuid.uuid4()) + if 'id' in cena_data: + self._scene_guid_map[cena_data['id']] = cena_guid + scene_payload = { "$type": "Scene", - "Guid": cena_data.get('guid'), + "Guid": cena_guid, "Operator": 6 if cena_data.get('scene_movers') else 1, "ParentSlot": None, "Unit": { @@ -1820,14 +1818,16 @@ def export_project(self): with open(args.input_json, 'r', encoding='utf-8') as f: project_data_from_json = json.load(f) - # Assume que a estrutura do seu JSON de entrada tem uma chave 'projeto' que contém os dados - # Se o seu JSON já é o objeto do projeto, use project_data_from_json diretamente - project_details = project_data_from_json.get('projeto', project_data_from_json) + # A estrutura do JSON de entrada é a própria raiz do projeto + project_details = project_data_from_json - # Criar uma instância do conversor e do projeto base + # Criar uma instância do conversor converter = RoehnProjectConverter() - # Os detalhes como nome do projeto, cliente, etc., devem estar no JSON de entrada - converter.create_project(project_details) + + # Usar os detalhes do projeto do JSON para criar a estrutura base + # A chave 'projeto' dentro do seu JSON contém os metadados + project_metadata = project_details.get('projeto', {'project_name': 'Projeto Importado'}) + converter.create_project(project_metadata) # Processar os dados do JSON para preencher o projeto Roehn converter.convert_project_from_json(project_details) diff --git a/cenario_simples.json b/cenario_simples.json new file mode 100644 index 0000000..b005c8e --- /dev/null +++ b/cenario_simples.json @@ -0,0 +1,240 @@ +{ + "id": 1, + "nome": "Projeto Cenario Simples", + "user_id": 1, + "status": "ATIVO", + "data_criacao": "2025-10-22T04:00:00.000000", + "data_ativo": "2025-10-22T04:00:00.000000", + "areas": [ + { + "id": 1, + "nome": "PAV-1", + "projeto_id": 1, + "ambientes": [ + { + "id": 1, + "nome": "Jantar", + "area_id": 1, + "circuitos": [ + { + "id": 1, + "identificador": "JNT-L01", + "nome": "Luz Principal Jantar", + "tipo": "luz", + "dimerizavel": false, + "potencia": 100.0, + "ambiente_id": 1, + "vinculacao": { + "id": 1, + "circuito_id": 1, + "modulo_id": 2, + "canal": 1, + "modulo": { + "id": 2, + "nome": "Modulo RL12" + } + } + }, + { + "id": 2, + "identificador": "JNT-L02", + "nome": "Luz Secundaria Jantar", + "tipo": "luz", + "dimerizavel": false, + "potencia": 100.0, + "ambiente_id": 1, + "vinculacao": { + "id": 2, + "circuito_id": 2, + "modulo_id": 2, + "canal": 2, + "modulo": { + "id": 2, + "nome": "Modulo RL12" + } + } + } + ], + "keypads": [ + { + "id": 1, + "nome": "Keypad A", + "modelo": "RQR-K", + "color": "WHITE", + "button_color": "WHITE", + "button_count": 2, + "hsnet": 110, + "dev_id": 110, + "ambiente_id": 1, + "notes": null, + "created_at": "2025-10-21T17:57:08", + "updated_at": "2025-10-21T17:57:08", + "buttons": [ + { + "id": 1, + "keypad_id": 1, + "button_index": 1, + "json_config": { + "EngraverText": "Luz 1", + "action": { + "type": "Toggle", + "target_type": "circuito", + "target_id": 1 + } + } + }, + { + "id": 2, + "keypad_id": 1, + "button_index": 2, + "json_config": { + "EngraverText": "Luz 2", + "action": { + "type": "Toggle", + "target_type": "circuito", + "target_id": 2 + } + } + } + ] + }, + { + "id": 2, + "nome": "Keypad Cenas", + "modelo": "RQR-K", + "color": "WHITE", + "button_color": "WHITE", + "button_count": 2, + "hsnet": 111, + "dev_id": 111, + "ambiente_id": 1, + "notes": null, + "created_at": "2025-10-22T09:30:00", + "updated_at": "2025-10-22T09:30:00", + "buttons": [ + { + "id": 3, + "keypad_id": 2, + "button_index": 1, + "json_config": { + "EngraverText": "Cena 1", + "action": { + "type": "Activate", + "target_type": "cena", + "target_id": 1 + } + } + }, + { + "id": 4, + "keypad_id": 2, + "button_index": 2, + "json_config": { + "EngraverText": "Tudo", + "action": { + "type": "Activate", + "target_type": "cena", + "target_id": 2 + } + } + } + ] + } + ], + "cenas": [ + { + "id": 1, + "guid": "a1b2c3d4-0001-4000-8000-111111111111", + "nome": "Cena Luz 1", + "scene_movers": false, + "acoes": [ + { + "id": 1, + "action_type": 0, + "target_guid": "1", + "level": 100 + } + ] + }, + { + "id": 2, + "guid": "a1b2c3d4-0002-4000-8000-222222222222", + "nome": "Cena Ligar Tudo", + "scene_movers": false, + "acoes": [ + { + "id": 2, + "action_type": 7, + "target_guid": "1", + "level": 100 + } + ] + } + ], + "quadros_eletricos": [] + } + ] + }, + { + "id": 2, + "nome": "Area Tecnica", + "projeto_id": 1, + "ambientes": [ + { + "id": 2, + "nome": "Quadro Geral", + "area_id": 2, + "circuitos": [], + "keypads": [], + "cenas": [], + "quadros_eletricos": [ + { + "id": 1, + "nome": "Quadro Principal", + "ambiente_id": 2, + "projeto_id": 1 + } + ] + } + ] + } + ], + "modulos": [ + { + "id": 1, + "nome": "Controlador M8", + "tipo": "ADP-M8", + "projeto_id": 1, + "is_controller": true, + "is_logic_server": true, + "ip_address": "192.168.0.245", + "quadro_eletrico_id": 1, + "parent_controller_id": null, + "child_modules": [ + { + "id": 2, + "nome": "Modulo RL12", + "tipo": "ADP-RL12", + "projeto_id": 1, + "is_controller": false, + "is_logic_server": false, + "ip_address": null, + "quadro_eletrico_id": 1, + "parent_controller_id": 1 + } + ] + }, + { + "id": 2, + "nome": "Modulo RL12", + "tipo": "ADP-RL12", + "projeto_id": 1, + "is_controller": false, + "is_logic_server": false, + "ip_address": null, + "quadro_eletrico_id": 1, + "parent_controller_id": 1 + } + ], + "keypads": [] +} \ No newline at end of file